diff --git a/pkg/js/CHANGELOG.md b/pkg/js/CHANGELOG.md index aeeb91ef855..403fafae99e 100644 --- a/pkg/js/CHANGELOG.md +++ b/pkg/js/CHANGELOG.md @@ -1,53 +1,42 @@ +## 0.6.7 + +- Remove `example` link to discontinued example. + ## 0.6.6 -* Add `@JSExport` annotation for exporting Dart classes and `@staticInterop` +- Add `@JSExport` annotation for exporting Dart classes and `@staticInterop` mocking. -* Require Dart 2.19 +- Require Dart 2.19 ## 0.6.5 -* Populate the pubspec repository field. -* Add a dependency on `package:meta`. -* Add an experimental `@trustTypes` annotation. +- Populate the pubspec repository field. +- Add a dependency on `package:meta`. +- Add an experimental `@trustTypes` annotation. ## 0.6.4 -* Includes `@staticInterop` to allow interop with native types from `dart:html`. +- Includes `@staticInterop` to allow interop with native types from `dart:html`. ## 0.6.3 -* Stable release for null safety. - -## 0.6.3-nullsafety.3 - -* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release - guidelines. - -## 0.6.3-nullsafety.2 - -* Allow prerelease versions of the `2.12` sdk. - -## 0.6.3-nullsafety.1 - -* Allow 2.10 stable and 2.11.0 dev SDK versions. - -## 0.6.3-nullsafety - -* Opt in to null safety. +- Stable release for null safety. +- Update SDK constraints to `>=2.12.0 <3.0.0`. ## 0.6.2 -* Improved documentation. +- Improved documentation. ## 0.6.1+1 -* Support Dart 2 final release. +- Support Dart 2 final release. ## 0.6.1 -* Add js_util library of utility methods to efficiently manipulate typed + +- Add js_util library of utility methods to efficiently manipulate typed JavaScript interop objects in cases where the member name is not known statically. ## 0.6.0 - * Version 0.6.0 is a complete rewrite of `package:js`. +- Version 0.6.0 is a complete rewrite of `package:js`. diff --git a/pkg/js/README.md b/pkg/js/README.md index db954322099..027f8c2f223 100644 --- a/pkg/js/README.md +++ b/pkg/js/README.md @@ -4,22 +4,17 @@ Use this package when you want to call JavaScript APIs from Dart code, or vice versa. -This package's main library, `js`, provides annotations and functions -that let you specify how your Dart code interoperates with JavaScript code. -The Dart-to-JavaScript compilers — dartdevc and dart2js — recognize these +This package's main library, `js`, provides annotations and functions that let +you specify how your Dart code interoperates with JavaScript code. The +Dart-to-JavaScript compilers — dartdevc and dart2js — recognize these annotations, using them to connect your Dart code with JavaScript. **Important:** This library supersedes `dart:js`, so don't import `dart:js`. Instead, import `package:js/js.dart`. -A second library in this package, `js_util`, provides low-level utilities -that you can use when it isn't possible to wrap JavaScript with a static, -annotated API. - -## Example - -See the [Chart.js Dart API](https://github.com/google/chartjs.dart/) for an -end-to-end example. +A second library in this package, `js_util`, provides low-level utilities that +you can use when it isn't possible to wrap JavaScript with a static, annotated +API. ## Usage @@ -70,9 +65,10 @@ class Location { ### Passing object literals to JavaScript Many JavaScript APIs take an object literal as an argument. For example: + ```js // JavaScript -printOptions({responsive: true}); +printOptions({ responsive: true }); ``` If you want to use `printOptions` from Dart a `Map` would be @@ -105,8 +101,8 @@ class Options { ### Making a Dart function callable from JavaScript -If you pass a Dart function to a JavaScript API as an argument, -wrap the Dart function using `allowInterop()` or `allowInteropCaptureThis()`. +If you pass a Dart function to a JavaScript API as an argument, wrap the Dart +function using `allowInterop()` or `allowInteropCaptureThis()`. To make a Dart function callable from JavaScript _by name_, use a setter annotated with `@JS()`. @@ -221,6 +217,7 @@ Note that you can have both `external` and non-`external` members in the extension. Compared to non-`@staticInterop` `package:js` classes, `@staticInterop` classes: + - Are more performant - Have better type guarantees - Generate less code @@ -228,11 +225,11 @@ Compared to non-`@staticInterop` `package:js` classes, `@staticInterop` classes: - Allow `external` extension members to be renamed using `@JS()` e.g. `@JS('renamedField')` -The only catch is that virtual/dynamic dispatch is *disallowed*. That means -methods are resolved using only the *static* type of the object. +The only catch is that virtual/dynamic dispatch is _disallowed_. That means +methods are resolved using only the _static_ type of the object. -In general, it's advised to use `@staticInterop` wherever you can, as future -JS interop will only target static dispatch. +In general, it's advised to use `@staticInterop` wherever you can, as future JS +interop will only target static dispatch. ### @JSExport and js_util.createDartExport @@ -302,7 +299,7 @@ getters, setters, and methods. That means you can’t export static members, constructors, factories, operators (the syntax complicates things), and extension methods. You can still have these members - they just won’t be present in the resulting exported object. Of course, you can use another instance member -to call these members as well, and *that* instance member will be exported. +to call these members as well, and _that_ instance member will be exported. In order to use `createDartExport`, you need to have a class that uses `@JSExport`.If you want to export only some members of a class, omit the @@ -335,7 +332,7 @@ essentially created a mock for `JSCounter`. In the past, to mock a plain `@JS` or `@anonymous` class, you could create a Dart class that `implements` that interop class, and due to Dart's virtual dispatch, this would call the Dart class' members instead. Now that we're using `external` extension members, this -no longer works. We now have to mock at the *JS level* instead. With +no longer works. We now have to mock at the _JS level_ instead. With `createDartExport`, you’re essentially using a Dart object to replace a JS object. This functionality is equivalent to mocking at the JS level, and you can also use it to mock the old non-`@staticInterop` `package:js` classes! @@ -357,7 +354,7 @@ tell you if you’ve got your mock class right. This is where `createStaticInteropMock` comes in. It takes in a separate type argument, e.g. `createStaticInteropMock(Counter())`, to -determine whether mocking *conformance* is satisfied. This type argument must be +determine whether mocking _conformance_ is satisfied. This type argument must be a `@staticInterop` class. With this, you’ll see an error saying that you haven’t implemented all the needed members. If the mock class implements all the needed members, the function does the same thing as `createDartExport`, and returns an @@ -386,7 +383,7 @@ extension B on StaticInterop { ``` This present an issue as a single Dart class cannot implement `member` as both a -field and a function. So, what to do? We require that you only implement *one* +field and a function. So, what to do? We require that you only implement _one_ of these members. So, either a Function field or a function are satisfactory. It is also sometimes desired that the mocking object is the same underlying type @@ -396,14 +393,14 @@ order to pass `instanceof` checks. In order to do this, we let users pass the JS prototype of the type they want the mocking object to be as an argument to `createStaticInteropMock`. -An important note here is that `createStaticInteropMock` looks for *all* +An important note here is that `createStaticInteropMock` looks for _all_ extensions of the `@staticInterop` type in the program, even if they are out of scope of the current file. In order to avoid a case where other libraries extending the `@staticInterop` type break your usage of `createStaticInteropMock`, you should try to only use this API in tests. `createStaticInteropMock` is meant to detect issues earlier at compile-time, but -if it's too restrictive, you can still use `createDartExport` to workaround -that (and please provide us feedback on why it's restrictive!). +if it's too restrictive, you can still use `createDartExport` to workaround that +(and please provide us feedback on why it's restrictive!). ## Reporting issues @@ -411,7 +408,6 @@ Please file bugs and feature requests on the [SDK issue tracker][issues]. [issues]: https://goo.gl/j3rzs0 - ## Known limitations and bugs diff --git a/pkg/js/example/README.md b/pkg/js/example/README.md deleted file mode 100644 index 6e5403e71fe..00000000000 --- a/pkg/js/example/README.md +++ /dev/null @@ -1 +0,0 @@ -See the [Chart.js Dart API](https://github.com/google/chartjs.dart/) for a usage example. diff --git a/pkg/js/lib/src/varargs.dart b/pkg/js/lib/src/varargs.dart deleted file mode 100644 index c3a4ec0f5d8..00000000000 --- a/pkg/js/lib/src/varargs.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -/// Declarations for variable arguments support -/// (rest params and spread operator). -/// -/// These are currently *not* supported by dart2js or Dartium. -library js.varargs; - -class _Rest { - const _Rest(); -} - -/// Annotation to tag ES6 rest parameters (https://goo.gl/r0bJ1K). -/// -/// This is *not* supported by dart2js or Dartium (yet). -/// -/// This is meant to be used by the Dart Dev Compiler -/// when compiling helper functions of its runtime to ES6. -/// -/// The following function: -/// -/// foo(a, b, @rest others) { ... } -/// -/// Will be compiled to ES6 code like the following: -/// -/// function foo(a, b, ...others) { ... } -/// -/// Which is roughly equivalent to the following ES5 code: -/// -/// function foo(a, b/*, ...others*/) { -/// var others = [].splice.call(arguments, 2); -/// ... -/// } -/// -const _Rest rest = _Rest(); - -/// Intrinsic function that maps to the ES6 spread operator -/// (https://goo.gl/NedHKr). -/// -/// This is *not* supported by dart2js or Dartium (yet), -/// and *cannot* be called at runtime. -/// -/// This is meant to be used by the Dart Dev Compiler when -/// compiling its runtime to ES6. -/// -/// The following expression: -/// -/// foo(a, b, spread(others)) -/// -/// Will be compiled to ES6 code like the following: -/// -/// foo(a, b, ...others) -/// -/// Which is roughly equivalent to the following ES5 code: -/// -/// foo.apply(null, [a, b].concat(others)) -/// -dynamic spread(args) { - throw StateError('The spread function cannot be called, ' - 'it should be compiled away.'); -} diff --git a/pkg/js/proposal.md b/pkg/js/proposal.md deleted file mode 100644 index 8fe6d0d55fd..00000000000 --- a/pkg/js/proposal.md +++ /dev/null @@ -1,1636 +0,0 @@ -# Better JavaScript and Dart interoperability - -Table of contents: - - - -- [Motivation](#motivation) -- [Typed APIs](#typed-apis) - * [Automatic Typed API Generation](#automatic-typed-api-generation) - * [Static Dispatch](#static-dispatch) - + [Extension Fields](#extension-fields) - * [JS Interop Checked Mode](#js-interop-checked-mode) - * [Data Type Interoperability](#data-type-interoperability) - * [Implicit and Explicit Conversions](#implicit-and-explicit-conversions) -- [Functions](#functions) - * [Optional Arguments](#optional-arguments) - * [Named Arguments](#named-arguments) - * [Overloads](#overloads) - * [This Argument](#this-argument) - * [Method Tearoffs](#method-tearoffs) - * [Generic Type Parameters](#generic-type-parameters) -- [Data Types Conversions](#data-types-conversions) - * [Generic Types, List and JS Array](#generic-types-list-and-js-array) - * [Null and Undefined](#null-and-undefined) - * [Future and JS Promise](#future-and-js-promise) - * [Iterable and JS Iterable](#iterable-and-js-iterable) - * [Stream and JS Async Iterable](#stream-and-js-async-iterable) - * [Stream and Callbacks](#stream-and-callbacks) - * [DateTime and JS Date](#datetime-and-js-date) - * [Map/Set and JS Map/Set](#mapset-and-js-mapset) - * [Maps and JS Objects](#maps-and-js-objects) - + [Wrapper-based JSObjectMap](#wrapper-based-jsobjectmap) - + [Autoboxing JSObjectMap](#autoboxing-jsobjectmap) -- [Less Static Interop](#less-static-interop) - * [Virtual Dispatch](#virtual-dispatch) - * [Interface and Dynamic dispatch](#interface-and-dynamic-dispatch) - * [JS Proxy](#js-proxy) - * [JS Reflection](#js-reflection) -- [Exporting Dart to JS](#exporting-dart-to-js) - * [Exporting classes and libraries](#exporting-classes-and-libraries) - * [Inheritance between Dart and JS](#inheritance-between-dart-and-js) - * [Interop with JS Modules](#interop-with-js-modules) - * [Exposing JS Methods as Getters](#exposing-js-methods-as-getters) -- [JS Types in the Dart Type System](#js-types-in-the-dart-type-system) -- [Implementation Roadmap](#implementation-roadmap) -- [Compatibility and Evolution](#compatibility-and-evolution) -- [FAQ](#faq) - * [Q: How does JSON work?](#q-how-does-json-work) - * [Q: Would new Dart language features help?](#q-would-new-dart-language-features-help) - * [Q: Can dart2js and dartdevc share implementation for JS interop?](#q-can-dart2js-and-dartdevc-share-implementation-for-js-interop) - * [Q: If a JS API returns "Object" does this break dart2js tree shaking?](#q-if-a-js-api-returns-object-does-this-break-dart2js-tree-shaking) - * [Q: Could we use dynamic interop instead?](#q-could-we-use-dynamic-interop-instead) - - - -## Motivation - -Better interoperability with JavaScript is one of the most highly requested -features for Dart. Previous work has made JS interop possible, but gaps remain. -This proposal outlines a series of usability improvements that, taken together, -make it much easier to use JS from Dart and vice versa. - -Here's an example of JS interop is like today, from the [Firebase package](https://pub.dev/packages/firebase): -```dart -import 'interop/app_interop.dart'; - -/// A Firebase App holds the initialization information for a collection -/// of services. -/// -/// See: . -class App extends JsObjectWrapper { - // [ed: doc comments removed to condense code] - static final _expando = Expando(); - - String get name => jsObject.name; - FirebaseOptions get options => jsObject.options; - - static App getInstance(AppJsImpl jsObject) { - if (jsObject == null) { - return null; - } - return _expando[jsObject] ??= App._fromJsObject(jsObject); - } - - App._fromJsObject(AppJsImpl jsObject) : super.fromJsObject(jsObject); - - Auth auth() => Auth.getInstance(jsObject.auth()); - Database database() => Database.getInstance(jsObject.database()); - Future delete() => handleThenable(jsObject.delete()); - Firestore firestore() => Firestore.getInstance(jsObject.firestore()); - - Storage storage([String url]) { - var jsObjectStorage = - (url != null) ? jsObject.storage(url) : jsObject.storage(); - return Storage.getInstance(jsObjectStorage); - } -} - -// [ed: in interop/app_interop.dart] - -@JS('App') -abstract class AppJsImpl { - external String get name; - external FirebaseOptions get options; - external AuthJsImpl auth(); - external DatabaseJsImpl database(); - external PromiseJsImpl delete(); - external StorageJsImpl storage([String url]); - external FirestoreJsImpl firestore(); -} -``` - -Nearly all of the Firebase classes are wrapped like that. It's a lot of work and -boilerplate to implement wrappers like this, and it adds overhead to every -operation. - -What we'd like to write is more like this: -```dart -@JS() -class App { - external String get name; - external FirebaseOptions get options; - - // Wrappers no longer necessary for these classes. - external Auth auth(); - external Database database(); - external Firestore firestore(); - - // Promise -> Future is handled automatically - external Future delete(); - - // Optional arguments are handled correctly by the compiler. - // (In advanced cases, there are ways to declare overloads.) - external Storage storage([String url]); -} -``` - -The [Google Maps API](https://github.com/a14n/dart-google-maps) package is -similar, but generated using the [js_wrapping](https://github.com/a14n/dart-js-wrapping) -code generator: - -```dart -@GeneratedFrom(_LatLngBounds) -@JsName('google.maps.LatLngBounds') -class LatLngBounds extends JsInterface { - LatLngBounds([LatLng sw, LatLng ne]) - : this.created(JsObject(context['google']['maps']['LatLngBounds'], - [__codec0.encode(sw), __codec0.encode(ne)])); - LatLngBounds.created(JsObject o) : super.created(o); - - bool contains(LatLng latLng) => - asJsObject(this).callMethod('contains', [__codec0.encode(latLng)]); - bool equals(LatLngBounds other) => - asJsObject(this).callMethod('equals', [__codec1.encode(other)]); - LatLngBounds extend(LatLng point) => __codec1 - .decode(asJsObject(this).callMethod('extend', [__codec0.encode(point)])); - LatLng get center => _getCenter(); - LatLng _getCenter() => - __codec0.decode(asJsObject(this).callMethod('getCenter')); - LatLng get northEast => _getNorthEast(); - LatLng _getNorthEast() => - __codec0.decode(asJsObject(this).callMethod('getNorthEast')); - LatLng get southWest => _getSouthWest(); - LatLng _getSouthWest() => - __codec0.decode(asJsObject(this).callMethod('getSouthWest')); - bool intersects(LatLngBounds other) => - asJsObject(this).callMethod('intersects', [__codec1.encode(other)]); - bool get isEmpty => _isEmpty(); - bool _isEmpty() => asJsObject(this).callMethod('isEmpty'); - LatLng toSpan() => __codec0.decode(asJsObject(this).callMethod('toSpan')); - String toString() => asJsObject(this).callMethod('toString'); - String toUrlValue([num precision]) => - asJsObject(this).callMethod('toUrlValue', [precision]); - LatLngBounds union(LatLngBounds other) => __codec1 - .decode(asJsObject(this).callMethod('union', [__codec1.encode(other)])); -} -``` - -What we'd like to write instead: -```dart -@JS('google.maps.LatLngBounds') -class LatLngBounds { - external LatLngBounds([LatLng sw, LatLng ne]); - - external bool contains(LatLng latLng); - external bool equals(LatLngBounds other); - external LatLngBounds extend(LatLng point); - external bool intersects(LatLngBounds other); - external LatLng toSpan(); - external String toString(); - external String toUrlValue([num precision]); - external LatLngBounds union(LatLngBounds other); - - @sealed - LatLng get center => _getCenter(); - @sealed - LatLng get northEast => _getNorthEast(); - @sealed - LatLng get southWest => _getSouthWest(); - @sealed - bool get isEmpty => _isEmpty(); - - @JS('getCenter') - external LatLng _getCenter(); - @JS('getNorthEast') - external LatLng _getNorthEast(); - @JS('getSouthWest') - external LatLng _getSouthWest(); - @JS('isEmpty') - external bool _isEmpty(); -} -``` - -The rest of the proposal outlines how we get there, as well as many other -usability and correctness improvements. - - -## Typed APIs - -The key principle behind Dart and JS interop is declaring API signatures, -as illustrated above. The two main pieces of this are `@JS()` annotations and -the `external` keyword: - -```dart -@JS('firebase.app') -library firebase.app; - -import 'package:js/js.dart' show JS; - -@JS('App') -class App { - external String get name; - // [...] -} -``` - -The `external` keyword marks a class member or library member as representing a -JavaScript API. It allows the body of the API to be omitted, and is only valid -on a declaration that is in a `@JS()` context (either it is marked with `@JS()` -itself, or is within a library/class that is marked). - -The `@JS()` annotation marks a declaration (library, class, library member or -class member) as representing a JavaScript API. An optional string can be -provided, to specify the name of the declaration in JS. This allows APIs to be -renamed. For example: - -```dart -@JS() -@anonymous -class UserInfo { - // [...] - - /// Returns a JSON-serializable representation of this object. - @JS('toJSON') - @JSConvert(dartify) - external Map toJson(); -} -``` - -In the example above `toJSON()` was named `toJson()` to match Dart naming -conventions. (This also implicitly marks it as `@sealed`.) - -Typed APIs provide many benefits: -- They assist discovery and use of the API, via autocompletion in the editor. -- They provide static type checking to help use the API correctly. -- They're the foundation of automatic data type conversions, and adding Dart - members to JS interop classes, to provide a more Dart-like API when desired. - - -### Automatic Typed API Generation - -Many JavaScript APIs have types now, either from TypeScript or Closure Compiler -comments. We'd like to have a tool to automatically generate -Dart interop APIs from those. The resulting file can then be hand edited, if -desired, to further customize the interop, or used immediately. - - -### Static Dispatch - -The example above renames *instance members*, allowing the API to be made -more Dart-friendly. This feature is crucial for interoperability, as the -JavaScript API may have a name that is not legal in Dart, or its name may have -different visibility, such as names starting with underscore. - -Renames are made possible by taking advantage of the static type of the -receiver. This is similar to static calls and extension methods. - -```dart -UserInfo user = someExpression; -print(user.toJson()); -``` - -The compiler only needs to generate something like: - -```js -let user = someExpression; -core.print(js.dartify(user.toJSON())); -``` - -Here a conversion was also inserted, based on the `@JSConvert(dartify)` -annotation. Because it's static, the compilers are able to inline JS code if -they choose to. - -Static dispatch lets us reshape APIs in very powerful ways, without the -overhead and complexity of wrappers. It also provides a good user experience -in the IDE. - -Notably, this interop is extremely similar to the language team's -[Static Extension Types](https://github.com/dart-lang/language/issues/42) and -[Static Extension Methods](https://github.com/dart-lang/language/issues/41) -proposals. All members in a JS interop class that have Dart implementation code -(i.e. function bodies, rather than `external`) are required to be nonextensible. -If we get those language features, they'll provide an alternate syntax, and -offer additional capabilities. - - -#### Extension Fields - -In existing web compilers, there is some ambiguity about how fields on JS types -are interpreted: - -```dart -@JS() -class MyJSClass { - external int get foo; - external set foo(int value); - - int bar; - - @JS() - int baz; -} -``` - -Both compilers support "foo". DDC interprets "bar" as a getter/setter pair -similar to "foo" but dart2js does not. Neither compiler recognizes the `@JS` on -"baz". - -Existing JS interop code suggests we need to provide two features: -- make it easy to declare external getter/setter pairs. -- add Dart state to JS classes. - -We can accomplish this by using `@JS` to indicate a JS field. With the new -interpretation: - -```dart -@JS() -class MyJSClass { - external int get foo; // external getter/setter - external set foo(int value); - - @sealed - int bar; // Dart extension field - - @JS() // sugar for external getter/setter, can't have an initializer - int baz; -} -``` - -The "extension field" is essentially an expando: - -```dart -@JS() -class MyJSClass { - // [...] - @sealed - int get bar => _bar[this]; - @sealed - void set bar(value) { _bar[this] = value; } -} - -final _bar = Expando(); -``` - -(Web compilers may implement it differently, such as a JS Symbol property stored -on the instance.) - - -### JS Interop Checked Mode - -We may want a "JS interop checked mode" to instrument APIs and check that -parameters and return types match their declarations. This would provide a lot -of added safety. It could be turned on for tests, for example. -If implemented, this would be an opt-in capability - - -### Data Type Interoperability - -To use typed APIs, we need to be able to pass data back and forth, and write -type annotations for the parameters and return value. One of the key questions -then is: can JS and Dart objects be passed directly, or is some type of -conversion or wrappers required? - -As the Firebase example illustrates, using wrappers everywhere has significant -cost in code size, performance, and usability. - -However, it is impossible to make every single Dart and JS object "just work" in -the other environment. Consider this example: - -```dart - external factory RecaptchaVerifier(container, - [@JSConvert(jsify) Map parameters, App app]); -``` - -While JavaScript has a [Map class](https://tc39.github.io/ecma262/#sec-map-objects) -now, many APIs still use raw JS Objects, such as `{ "size": "invisible" }`. In -Dart `Map` is an interface that at runtime, could contain any -class that implements Map (including user-defined Maps). Dart's equality -semantics (`operator ==` and `hashCode`) are different from JS too. - -(Even if we're only interested in Dart's default LinkedHashMap with String -keys, it would be difficult to make this work. The Map class would need to -store its data as properties on itself. Some keys would not work correctly such -as "__proto__", and some operations would have different performance -characteristics. The map can be modified from JS, so it's difficult to see how -things like `length` and `keys` could be implemented efficiently.) - -For these reasons, we can't take an all-or-nothing approach: ideally most types -are wrapperless, but we'll need good support for conversions too. - - -### Implicit and Explicit Conversions - -Wrapperless types are very convenient: you pass them back and forth, and they -just work. Explicit conversions involve a lot of boilerplate that must be -repeated everywhere. For example, we could've written the earlier `toJson()` -example like this: - -```dart -@JS() -@anonymous -class UserInfo { - // [...] - - /// Returns a JSON-serializable representation of this object. - @sealed - Map toJson() => dartify(_toJson()); - - @JS('toJSON') - external Map _toJson(); -} -``` - -Or worse, the original version of the Recaptcha example shown earlier: -```dart -factory RecaptchaVerifier(container, - [Map parameters, App app]) => - (parameters != null) - ? ((app != null) - ? RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl( - container, jsify(parameters), app.jsObject)) - : RecaptchaVerifier.fromJsObject( - RecaptchaVerifierJsImpl(container, jsify(parameters)))) - : RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(container)); - -// [ed: in interop/auth_interop.dart] - - external factory RecaptchaVerifierJsImpl(container, - [Object parameters, AppJsImpl app]); -``` - -Implicit conversions reduce the boilerplate and make the types feel more -like directly passed (wrapperless) types. They require less developer input to -get the correct conversion. The downside is that the conversion is less visible, -and the implications of conversions may surprise the developer. Overall these -conversion are similar to "autoboxing" in C# and Java, and are probably a net -usability benefit. - -There's a language issue for [implicit conversions](https://github.com/dart-lang/language/issues/107), -so that may provide us better support. The native FFI also has a similar -mechanism for marshalling. - - -## Functions - -Similar to Array, functions from JS do not have reified type information and are -allowed to be cast to any Dart function. This is already implemented in Dart web -compilers. - -Ideally Dart functions could also be directly passed to JS. However this is not -the case in dart2js. It is not simple to change that, however. - -In the meantime, automatic converisons will be provided: -- passing a Dart function type to any JS parameter performs an `allowInterop` - conversion. -- passing any Dart value to a JS function type performs an `allowInterop` - conversion if the runtime value is a Dart function. -- Function typed parameters/variables can be annotated with `@JS()` to indicate - that they should be treated as being passed to JS, implying an `allowInterop` - conversion. -- Typedefs can be annoated with `@JS()` to indicated they should be treated as - being passed to JS, implying an `allowInterop` conversion. - -One particular challenge is that dartdevc (DDC) represents Dart functions as JS -functions, so it is difficult to catch problems that may arise later in dart2js. -It should not occur much in practice, thanks to implicit conversions, and the -previously mentioned "checked mode" provides a means to catch it. - - -### Optional Arguments - -To pass functions between JS and Dart, we need to define how calling conventions -work. The general principle is "when calling JS, work like JS". Consider the -first example again: - -```dart - Storage storage([String url]) { - var jsObjectStorage = - (url != null) ? jsObject.storage(url) : jsObject.storage(); - return Storage.getInstance(jsObjectStorage); - } - - // What we'd like to write: - external Storage storage([String url]); -``` - -Or for a more complex case, consider the Recaptcha example: -```dart -factory RecaptchaVerifier(container, - [Map parameters, App app]) => - (parameters != null) - ? ((app != null) - ? RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl( - container, jsify(parameters), app.jsObject)) - : RecaptchaVerifier.fromJsObject( - RecaptchaVerifierJsImpl(container, jsify(parameters)))) - : RecaptchaVerifier.fromJsObject(RecaptchaVerifierJsImpl(container)); - -// [ed: in interop/auth_interop.dart] - external factory RecaptchaVerifierJsImpl(container, - [Object parameters, AppJsImpl app]); -``` - -These methods are dispatching the call manually, to handle the difference in -JS and Dart calling conventions: optional arguments are not passed as `null` -in JS, but are visible via `arguments.length` and their value defaults to -`undefined` rather than `null`. - -What we'd like to write for Recaptcha is: -```dart - external factory RecaptchaVerifier(container, - [@JSMapToObject() Map parameters, App app]); -``` - -Because it's now a JS function, the compilers must call it with the correct -number of arguments. For example, this Dart code: -```dart -RecaptchaVerifier(container); -RecaptchaVerifier(container, {'foo': 'bar'}); -RecaptchaVerifier(container, {'foo': 'bar'}, app); -``` - -Should compile to JS code similar to: -```js -new auth.RecaptchaVerifier(container); -new auth.RecaptchaVerifier(container, {'foo': 'bar'}); -new auth.RecaptchaVerifier(container, {'foo': 'bar'}, app); -``` - -If someone wants to forward multiple parameters, we could provide an annotation -to help: - -```dart -@JS() -class Example { - @sealed - Object wrapsMethodWithOptionalArgs([Object foo, Object bar]) { - // [...] - return _underlyingJSMethod(foo, bar); - } - - @JS('underlyingJSMethod') - external Object _underlyingJSMethod( - [@JSNullToOptional() Object foo, @JSNullToOptional() Object bar]); -} -``` - -With `@JSNullToOptional()` the compiler would automatically check for `null` in -those arguments and dispatch the call to the correct JS signature. - - -### Named Arguments - -JS functions don't have a notion of named arguments; rather by convention, JS -Object literals are passed, and optionally destructured into parameters by the -callee. - -Named arguments will be allowed as syntactic sugar for passing a JS object -literal. For example: - -```dart -class Example { - external takesJSObject({String a, int b}) -} -f(Example e) { - e.takesJSObject(a: 'hi', b: 123); -} -``` - -Is equivalent to this JS: - -```js -function f(e) { - e.takesJSObject({a: 'hi', b: 123}); -} -``` - -Similarly if a Dart function that takes named arguments is passed to JS, it -must desugar them. To make this work, we'll need to restrict JS function types -to having only optional or named arguments, not both. - - -### Overloads - -Method overloads do not exist in JavaScript. They can be helpful for -expressing type signatures, and calling native APIs that are -overloaded, such as Canvas's [createImageData](https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation). - -Overloads can be expressed by declaring multiple Dart methods and giving them -the same JavaScript name: - -```dart -// in CanvasRenderingContext2D - @JS('createImageData') - external ImageData createImageDataFromImage(ImageData imagedata); - @JS() - external ImageData createImageData(int sw, int sh); -``` - -If the author wanted to expose those as the same method, they could instead do: - -```dart - @JS() - external ImageData createImageData(imagedata_OR_sw, [int sh]); -``` - -Dispatching based on types should not generally be required, but if it is, it -can be written like this: - -```dart - @sealed - ImageData createImageData(imagedata_OR_sw, [int sh]) { - if (imagedata_OR_sw is int && sh != null) { - return _createImageData(imagedata_OR_sw, sh); - } else { - return _createImageDataFromImage(imagedata_OR_sw); - } - } - JS('createImageData') - external ImageData _createImageDataFromImage(ImageData imagedata); - @JS() - external ImageData _createImageData(int sw, int sh); -``` - -The language team is also considering the problem of declaring Dart APIs like -this, see [issue 145](https://github.com/dart-lang/language/issues/145). - - -### This Argument - -JS functions have a hidden `this` parameter, and it is sometimes important for -Dart to be able to pass that to JS, or receive it from JS. - -That's accomplished with the `@JSThis()` annotation: - -```dart -@JS('Object') -abstract class JSObject { - external static _JSObjectPrototype prototype; -} - -@JS() -@anonymous -abstract class _JSObjectPrototype { - external static bool hasOwnProperty(@JSThis() target, propertyKey); -} - -@JS() -@anonymous -class PropertyDescriptor { - @JS() Object value; - @JS() bool writable; - @JS() bool configurable; - @JS() bool enumerable; - external Object Function(@JSThis() Object) get; - external void Function(@JSThis() Object, Object) set; -} -``` - -This lets you declare which parameter should receive `this`/be passed as `this`. - - -### Method Tearoffs - -Tearoffs of JS types should bind `this`, as noted in -[issue 32370](https://github.com/dart-lang/sdk/issues/32370). We also need to -decide what runtime type information to attach. Tearoffs could get the -statically visible type at the call site, or they could be treated like other -JS functions, and be assignable to any function type. Untyped is advantageous -for performance/simplicity, so it's probably preferable, unless we find -compelling examples. - - -### Generic Type Parameters - -JavaScript does not have reified generic types. Generic types can -be used in interop signatures, but they have no effect at runtime, and no type -argument is passed. - -For JS code calling into exported Dart functions (e.g. `@JSExport`), generic -type arguments will come through as a special, runtime-only type `JSAny` that -represents the absence of a reified generic type. This is discussed further in -the next section, about Generic Types, List and JS Array. - - -## Data Types Conversions - -This section discusses specific data types, and has recommendations for how -these should be handled in the Dart web compilers - -### Generic Types, List and JS Array - -Both Dart web compilers already use JS Array to implement Dart `List` in -a wrapperless style. The main challenge for JS interop is how to handle generic -types. - -Generic types in Dart are reified, in other words, generic type arguments are -represented and stored at run time. These are used by `is` and `as` checks, for -example, `[1, 2, 3] is List` will return true, and the cast -`[1, 2, 3] as List` will succeed. But `[1, 2, 3] is List` -will return false. - -JavaScript cannot create Arrays that have an associated Dart generic type. -Currently these are interpreted as `List`, which frequently results in -a cast failure, or requires a workaround. For example: -```dart -List get apps => firebase.apps - // explicitly typing the param as dynamic to work-around - // https://github.com/dart-lang/sdk/issues/33537 - .map((dynamic e) => App.getInstance(e)) - .toList(); - -// [in another file] - -@JS() -external List get apps; -``` - -The problem is that `List get apps` actually returns `List`, -so calls like `.map` will fail Dart's reified generic checks (because the list -could contain anything, the parameter `e` must be typed to accept anything). -This leads to other strange results, such as: - -```dart -@JS() -external List get stringsFromJS; - -main() { - // Either prints `true` or `false`! - // True if the compiler optimizes it, false if it's evaluated at runtime. - print(stringsFromJS is List); - - List list = stringsFromJS; - List list2 = list; // type error: not a List. -} -``` - -The new version of this API is simply: -```dart -external List get apps; -``` - -And it works mostly how you'd expect with `is` and `as` checks: - -```dart -main() { - List apps = firebase.apps; - print(apps is List); // true - apps as List; // works - - // prints list of names names, `(a)` is inferred as `(App a)` - print(apps.map((a) => a.name).toList()); - - apps is List // true: `int` could be from JS - apps is List // false: can't be a list of Dart classes - // (unless the class is exported to JS) -} -``` - -The last check is an example of one consequence of the looser checking. -Conceptually we have a `JSAny` type. This type only exists in runtime, and does -not require a representation, since it results from the absence of type -information. This is discussed later when we look at the type system. - -Besides JS Arrays, Dart generic functions and methods can also be called from -JS. In that case, the reified generic type will usually be omitted. This is -discussed later when we look at exporting Dart classes to JS. - - -### Null and Undefined - -Dart web compilers generally treat these as interchangeable; while they don't -create `undefined` themselves, it's treated as `== null`. This makes interop -easier, so it's probably worth keeping. We should also add an `undefined` -constant to "package:js", for cases where it's necessary to pass it explicitly -to JS. - - -### Future and JS Promise - -JS Promises are very common in web APIs, as many operations are asynchronous. -These function very similarly to Dart's `Future` interface. - -There are several possible answers: -1. provide an implicit conversion -2. both types implement the other's interface -3. have _Future be a JS Promise - -Option 3 is not feasible. Option 2 is ideal, but may cause issues in dart2js, -because they currently assume no JS types implement `Future` (this avoids -"getInterceptor" overhead). So we probably need to go with Option 1. The -dart:html library already has a conversion in one direction, and `dartdevc` may -soon have both directions (to use JS async/await). - -If we did want to do the adapter design, here is a rough sketch: - -```dart -@JS('Promise') -@JSInterfaceDispatch() -@JSDynamicDispatch() // Note: because existing SDK native types support this. -class JSPromise implements Future { - @JS('then') - external JSPromise _then( - /*R | Thenable*/Object Function(T) onFulfilled, - [/*R | Thenable*/Object Function(Object) onRejected]); - - external factory JSPromise.resolve(/*R | Thenable*/Object value); - - Future then(FutureOr onValue(T value), {Function onError}) { - return _then( - Zone.current.bindUnaryCallback(onValue), - onError != null - // Note: real impl must also support an `onError` callback that - // takes a StackTrace as the second argument. - ? Zone.current.bindUnaryCallback(onError as dynamic) - : null); - } - // [...] -} - -class _Future implements Future { - // [...] - - // Note: in reality this could be injected by the compiler on any class - // implementing Future if we want them to be Promise-like. - @JSExport('then') - JSPromise _jsThen( - /*R | Thenable*/Object Function(T) onFulfilled, - [/*R | Thenable*/Object Function(Object) onRejected]) { - - Future f = then((value) => JSPromise.resolve(onFulfilled(value)), - onError: onRejected != null - ? (error) => JSPromise.resolve(onRejected(error)) - : null); - // This conversion is not necessary if we only want to implement Thenable. - return JSPromise.resolve(f); - } -} -``` - - -### Iterable and JS Iterable - -All Dart `Iterable` classes should implement `[Symbol.iterator]`, which -allows them to be used in JS `for-of` loops. - -Converting from a JS iterable to a Dart Iterable requires a conversion -(either implicit or explicit). - -Avoiding a conversion is probably not ideal. We'd need `Iterable` methods -on any JS object that contains `[Symbol.iterator]`. This requires compilers to -place `Iterable` members on Object.prototype, or handle this at the -interceptor level. The current theory is that not many JS APIs return -iterables (Arrays are much more common). A wrapper-based conversion, either -implicit or explcit, should be enought to handle this. - - -### Stream and JS Async Iterable - -The EcmaScript draft contains a new feature [for-await-of loops](https://tc39.github.io/ecma262/#sec-for-in-and-for-of-statements) -and `Symbol.asyncIterator` (see also [MDN for-await-of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of)). -Once JS has that, `Stream` could work similarly to `Future`. - - -### Stream and Callbacks - -These will require a conversion in either direction. However we can provide -helpers to make this easy. Consider this example from Firebase: - -```dart -class Auth extends JsObjectWrapper { - // [...] - Func0 _onAuthUnsubscribe; - StreamController _changeController; - - Stream get onAuthStateChanged { - if (_changeController == null) { - var nextWrapper = allowInterop((firebase_interop.UserJsImpl user) { - _changeController.add(User.getInstance(user)); - }); - - var errorWrapper = allowInterop((e) => _changeController.addError(e)); - - void startListen() { - assert(_onAuthUnsubscribe == null); - _onAuthUnsubscribe = - jsObject.onAuthStateChanged(nextWrapper, errorWrapper); - } - - void stopListen() { - _onAuthUnsubscribe(); - _onAuthUnsubscribe = null; - } - - _changeController = StreamController.broadcast( - onListen: startListen, onCancel: stopListen, sync: true); - } - return _changeController.stream; - } -} - -// [in interop/auth_iterop.dart] -@JS('Auth') -class AuthJsImpl { - // [...] - external Func0 onAuthStateChanged(nextOrObserver, - [Func1 opt_error, Func0 opt_completed]); -} -``` - -With the right helpers and elimination of wrappers, this becomes: - -```dart -@JS() -class Auth { - @sealed - Stream _changeStream; - - @JS('onAuthStateChanged') - external Func0 _onAuthStateChanged(Func1 nextOrObserver, - [Func1 opt_error, Func0 opt_completed]); - - @sealed - Stream get onAuthStateChanged => - _changeStream ??= CreateStream(_onAuthStateChanged); -} - -// Note: package:js will defined some helpers like this. -// More research is needed to find all of the common patterns. -static Stream CreateStream( - @JS() Func0 Function(Func1 nextOrObserver, [Func1 opt_error]) - subscribeToEvent) { - Func0 unsubscribe; - StreamController controller; - controller = StreamController.broadcast( - onListen: () { - // Because `subscribeToEvent` is annotated with `@JS()`, `allowInterop` - // will be automatically handled on these callbacks. - unsubscribe = subscribeToEvent(controller.add, controller.addError); - }, - onCancel: unsubscribe, - sync: true); - return controller.stream; -} -``` - - -### DateTime and JS Date - -Similar to Future/Promise, we can investigate and determine which of these is -best: -- implement DateTime on top of JS Date -- have them implement each other's interfaces -- provide an implicit conversion - - -### Map/Set and JS Map/Set - -At the very least, we'll provide `JSMap` and `JSSet` types in -"package:js" that will implement the corresponding Dart interfaces and also be -wrapperless JS Maps/Sets. - -Both web compilers already use (or have prototyped use of) ES6 Map/Set under -some circumstances, such as identity Maps/Sets. So it may be possible to have -the objects returned by `HashMap.identity()` and `HashSet.identity()` simply be -JS Map/Set respectively. That would be a nice feature, but is not necessary -to provide good interop with these types. - -Declaring a JS API as returning a Dart Map will be a hint, because it will -probably not work the way the developer expects (if they are expecting a JS -object to be interpreted as a Map). Instead they can: -- use `JSMap` if they intended the JS Map class. -- use `JSObjectMap` if the return value is a JS object. It's a normal Dart - class that implements Map and is backed by the JS Object. This indicates a - wrapping conversion. -- use `@JSConvert()` to provide a custom conversion to Map -- use some other JS class type, which declares the properties that the map - contains. Useful when a JS Object is returned, and the data is structured. - -Open question: are the generic type arguments to JSMap/JSSet reified if -allocated in Dart? I think they should be, for consistency with `List`. -Similar to `List` however, a `Map` allocated in JS could be cast to -`JSMap` for any types `K` and `V` that subtype `JSAny`. - - -### Maps and JS Objects - -Converting arbitrary Dart maps to JS Objects is probably not feasible, for -reasons discussed in "Data Type Interoperability". However we can provide -a conversion: - -```dart - external factory RecaptchaVerifier(container, - [@JSConvert(jsify) Map parameters, App app]); -``` - -The `jsify` function is declared by Firebase, and does a deep conversion based -on the specific types Firebase APIs use. However most of that will no longer be -necessary, so we might be able to get away with: - -```dart - external factory RecaptchaVerifier(container, - [@MapToJSObject() Map parameters, App app]); -``` - -This uses a built-in shallow conversion from a Dart Map to a JS object. - - -#### Wrapper-based JSObjectMap - -To make it easier to work with JS objects via indexing, we could provide several -classes in "package:js" to help: - -```dart -@JS() -@anonymous -class JSIndexable { - external V operator [](K key); - external void operator []=(K key, Object value); - - Map toMap() => JSObjectMap(this); -} - -/// Wraps a JS Object in a Dart Map. -class JSObjectMap with MapMixin implements Map { - final JSIndexable object; - - JSObjectMap([Object object]) - : object = object as JSIndexable ?? JSIndexable(); - - factory JSObjectMap.from(Map other) => JSObjectMap()..addAll(other); - factory JSObjectMap.of(Map other) = JSObjectMap.from; - factory JSObjectMap.fromEntries(Iterable> entries) => - JSObjectMap()..addEntries(entries); - - bool containsKey(Object key) => Reflect.has(object, key); - V operator [](Object key) => object[key]; - void operator []=(Object key, value) { - object[key] = value; - } - List get keys => Reflect.ownKeys(object) as List; - V remove(Object key) { - if (Reflect.has(object, key)) { - var result = object[key]; - Reflect.deleteProperty(object, key); - return result; - } - return null; - } - void clear() { - for (var key in Reflect.ownKeys(object)) { - Reflect.deleteProperty(object, key); - } - } -} -``` - -Here `JSObjectMap` works like a normal Dart class, but it's easy to get the -raw JS object from it. Meanwhile any JS object can be cast to `JSIndexable` to -provide access to the index operators and the `toMap()` function. - - -#### Autoboxing JSObjectMap - - -*NOTE*: we probably won't want/need this, but I wanted to mention it as a -possible optoon. It provides a means of implementing Dart interfaces from JS -classes, that is reasonably friendly to optimizations. - -An alternative design for `JSObjectMap` is to work similarly to `JSIndexable`. -This provides a Map-like view of an arbitrary JS object. Then the question -becomes: how do we cast our type to a `Map`? Autoboxing would allow us to -do that. - -Here's a rough sketch: - -```dart -@JS() -@JSAutobox() -class JSObjectMap extends MapBase { - factory JSObjectMap() => _create(null); - factory JSObjectMap.from(Map other) => _create(null)..addAll(other); - factory JSObjectMap.of(Map other) = JSObjectMap.from; - factory JSObjectMap.fromEntries(Iterable> entries) => - _create(null)..addEntries(entries); - - @JS('create') - external static JSObjectMap _create(Object proto); - - external V operator [](Object key); - external void operator []=(Object key, Object value); - - bool containsKey(Object key) => Reflect.has(this, key); - List get keys => Reflect.ownKeys(this) as List; - - V remove(Object key) { - if (Reflect.has(this, key)) { - var result = this[key]; - Reflect.deleteProperty(this, key); - return result; - } - return null; - } - - void clear() { - for (var key in Reflect.ownKeys(this)) { - Reflect.deleteProperty(this, key); - } - } -} -``` - -Because this type uses `@JSAutobox()`, the JS object will be automatically -boxed when cast to any Dart interface that it implements (except for dart:core -Object). This reduces the boilerplate that might otherwise be required. - -The benefit of this approach is that any JS object can be freely cast to -`JSObjectMap`, providing efficient access using Map-like APIs. For example, -let's revist our `UserInfo.toJson()` example: - -```dart -@JS() -@anonymous -class UserInfo { - // [...] - /// Returns a JSON-serializable representation of this object. - @JS('toJSON') - external JSObjectMap toJson(); -} - -// [code snippet from auth_test.dart] - - test('toJson()', () async { - // [...] - var userMap = userCredential.user.toJson(); - expect(userMap, isNotNull); - expect(userMap as Map, isNotEmpty); // [note: `as Map` was added] - expect(userMap["displayName"], "Other User"); - expect(userMap["photoURL"], "http://google.com"); - // [...] - }); -``` - -Consider the line `expect(userMap as Map, isNotEmpty)` in this example. -The `as Map` is necessary to trigger boxing, so the `isNotEmpty` matcher will -work. - -Autoboxing would be a neat way to provide support for implementing Dart -interfaces from JS. Autoboxing has proven useful in other languages for native -interop. It may be useful for advanced JS interop scenarios, such as providing -a dart:html-like API without JS dynamic/interface dispatch. - - -## Less Static Interop - -### Virtual Dispatch - -In many cases JS interop can take advantage of virtual dispatch, even with -`@sealed`, because the JS method it calls will dispatch with normal JS rules -(i.e. lookup the property on the prototype chain). `@sealed` procludes overrides -of itself with another Dart member, however. - -We support virtual Dart methods on JS types, with a bit more work. This doesn't -compromise the model very much, but it does add additional dispatch cost. -Implementing interfaces and dynamic dispatch would still be restricted. - - -### Interface and Dynamic dispatch - -In the future, we may want to provide something like `@JSInterfaceDispatch()` or -`@JSDynamicDispatch()`, that preserves RTTI and enables interface/dynamic -dispatch. Our compilers already support this, as they use it themselves for core -types in the SDK, and for the HTML libraries. - -There are several issues with exposing this: -- extensions would now have to be globally unique for a given JS prototype. -- this requires compile time checking (dart2js) or runtime checking (dartdevc). -- it's possible to subvert compile time checking accidentally (if the same JS - prototype is exposed with two different names). -- we'd need to standardize the annotations between the compilers and ensure - they have the same semantics. - -Scoped extension method approaches for dynamic languages may help for the naming -conflicts (e.g. -[as discussed in this paper](https://arxiv.org/pdf/1708.01679.pdf)), -but those have performance/complexity/usability tradeoffs. - -Many users have requested flags to disable dynamic dispatch, so this may not be -the direction we need go. Implementing interfaces can be useful however. -(The "autoboxing" approach discussed previously may be a possible compromise.) - - -### JS Proxy - -One question that comes up occasionally is how to use JS Proxy from Dart. -We can expose JSProxy from "package:js": - -```dart -@JS('Proxy') -class JSProxy { - external factory JSProxy(Object target, JSProxyHadler handler); -} - -@JS() -@anonymous -abstract class JSProxyHandler { - // Note: these are defined as field to allow only a subset of them to be - // implemented - @JS() - Object Function(Object target) getPrototypeOf; - - @JS() - Object Function(Object target, Object proto) setPrototypeOf; - - // [...] - - @JS() - bool Function(Object target, Object key) has; - - @JS() - Object Function(Object target, Object key) get; - - @JS() - Object Function(Object target, Object key, Object value) set; - - @JS() - bool Function(Object target, Object key) deleteProperty; - - external factory JSProxyHandler({ - this.getPrototypeOf, - this.setPrototypeOf, - this.get, - this.set, - this.has, - this.deleteProperty /* [...] */}); -} -``` - -It's possible to implement a proxy handler that forwards to Dart's -noSuchMethod, or the reverse, if someone wanted to do that. - -(It's also possible to have a noSuchMethod that uses JS Reflect and Object APIs -to access the JS object. It's not necessary though, because `@JS()` classes are -essentially a more optimized form of that.) - - -### JS Reflection - -JS Reflection APIs are useful for low level support, and these can be provided -by "package:js": - -```dart -@JS('Reflect') -class JSReflect { - external static Object apply(target, thisArgument, argumentsList); - external static Object construct(target, argumentsList, [newTarget]); - external static bool defineProperty( - target, propertyKey, PropertyDescriptor attributes); - external static bool deleteProperty(target, propertyKey, [receiver]); - external static Object get(target, propertyKey); - external static PropertyDescriptor getOwnPropertyDescriptor( - target, propertyKey); - external static Object getPrototypeOf(target); - external static bool has(target, propertyKey); - external static bool isExtensible(target); - external static List ownKeys(target); - external static void preventExtensions(); - external static set(target, propertyKey, value, [receiver]); - external static setPrototypeOf(target, prototype); - - static bool hasOwnProperty(target, propertyKey) { - // Note: could also be implemented using `getOwnPropertyDescriptor` - return _JSObject.prototype.hasOwnProperty(target, propertyKey); - } - - static Object getOwnProperty(target, propertyKey) { - var desc = getOwnPropertyDescriptor(target, propertyKey); - if (desc == null) return null; - if (desc.get != null) return desc.get(target); - return desc.value; - } -} - -@JS('Object') -abstract class _JSObject { - external static _JSObjectPrototype prototype; -} - -@JS() -@anonymous -abstract class _JSObjectPrototype { - external static bool hasOwnProperty(@JSThis() target, propertyKey); -} - -@JS() -@anonymous -class PropertyDescriptor { - Object value; - bool writable; - bool configurable; - bool enumerable; - Object Function(@JSThis() Object) get; - void Function(@JSThis() Object, Object) set; -} -``` - - -## Exporting Dart to JS - -Dart functions and accessors can be exported to JS with `@JSExport`. A compiler -must provide a version of the function that is callable via the JS interop -calling conventions described earlier, and ensure that version of the function -is used when it is passed to JavaScript. (It may have other versions of the -function that use optimized, dart-specific calling conventions.) - - -### Exporting classes and libraries - -This will use `@JSExport` similar to top-level functions/accessors. - -TODO: more details here - -### Inheritance between Dart and JS - -Conceptually extending a JS class from Dart is similar to adding methods, -because `super` calls are statically dispatched. The tricky part is what to do -with constructors. JS constructors are normally written as `external factory` -which precludes extending them. Also there are some notable differences in -field initialization+super constructor call order between Dart and JS. - -(JS requires super before initializing fields, Dart requires super after -initializing final fields. Dart's approach is nice because it prevents -virtual methods from observing uninitialized state. But for interop, the problem -is that the two orders are incompatible.) - -We may be able to provide a method that creates your class: - -```dart -class MyDartClass extends TheirJSClass { - final int x = 1; - int y, z; - factory MyDartClass(int y, int z) { - // super constructor parameters passed here? - var self = createJS(); - self.y = y; - self.z = z; - return self; - } - // ... -} -``` - -It's not the most satisfying solution, but it seems like a relatively easy way -to support this. - -TODO: extending a Dart class from JS - - -### Interop with JS Modules - -We'll need a way to declare a JS interop library corresponds to a JS module. -This could be done on the library tag: - -```dart -@JSImport('goog.editor', moduleFormat: 'closure') -library goog.editor; -``` - -The compiler can then generate the appropriate import, instead of a global -reference. This will need to be coordinated with the overall build and server -system, however, to ensure the JS module is available to the module loader -(requirejs, ES6 imports, etc). - -Typically the module format will be assumed to be passed in globally to the -compiler, as there is generally one standardized module format, so that argument -won't be present. (Closure library is illustrated here as it can be used in -addition to other formats, it may be useful to tell the compiler "this module -is definitely a closure namespace, import it as such".) - - -### Exposing JS Methods as Getters - -One common pattern that comes up is converting "getX" methods into "get X" -getters. We could add `@JSMethod` syntactic sugar to simplify that. -Consider this prior example of a Google Maps API: - -```dart - LatLng get center => _getCenter(); - LatLng get northEast => _getNorthEast(); - LatLng get southWest => _getSouthWest(); - bool get isEmpty => _isEmpty(); - - @JS('getCenter') - external LatLng _getCenter(); - @JS('getNorthEast') - external LatLng _getNorthEast(); - @JS('getSouthWest') - external LatLng _getSouthWest(); - @JS('isEmpty') - external bool _isEmpty(); -``` - -It could be written as: - -```dart - @JSMethod('getCenter') - external LatLng get center; - - @JSMethod('getNorthEast') - external LatLng get northEast; - - @JSMethod('getSouthWest') - external LatLng get southWest; - - @JSMethod('isEmpty') - external bool get isEmpty; -``` - -## JS Types in the Dart Type System - -As discussed earlier, at runtime the absence of reified type information will be -tracked with a `JSAny` type. `JSAny` can contain types that may originate from -JS, and it only exists at run time. Here are some examples: - -```dart -import 'package:firebase/firebase.dart' show App, Database; -import 'package:firebase/firebase.dart' as firebase; -main() { - List apps = firebase.apps; - apps is List // true: Object can hold JS types - apps is List // true: `int` could be from JS - apps is List // true: `List` could be from JS - apps is List // true: could be a list of any JS interop types - apps is List // false: can't be a Dart class - apps is List // false: can't be a Dart class like Stopwatch -} -``` - -We'll probably want to provide access to JS `typeof` and `instanceof` as library -functions in "package:js". - -The proposed typing rules for JSAny allow it assigned to/from these types: -- primitives: Null | bool | int | num | double | String -- JS interop classes (`@JS()`) -- Dart classes exported to JS (`@JSExport()`) -- `List` -- other "native" SDK types -- functions (restrict to JSAny param/return types?) - -This type will not exist in the static type system. The hope is that these -restrictions keep the unsoundness restricted to objects allocated from JS, and -to types that are likely to be used by existing JS APIs. By excluding Dart -classes, we're able to catch things like `List` assigned to -`List`, which is unlikely to work. - -Open question: should preserve reified JSAny if that type parameter is used to -construct other types? For example: - -```dart -@JS() -external List foo(); -bar() { - var list = foo(); - var set = list.toSet() as Set; // does this work? - // ... -} -``` - - -## Implementation Roadmap - -Here's a rough breakdown of the features into different categories. -Details subject to change. The items are not in any particular order. - -Required features (P0): -- static dispatch (with rename support) -- static type checking rules (warnings/errors for incorrect usage) -- runtime type checking rules -- conversions for builtin types: - - Functions - - Dart Map to/from JS Object - - Future/Promise -- calling conventions for optional arguments, "this" -- exporting top-level functions, accessors to JS - -Important features (P1): -- user defined conversions -- conversions for types (e.g. DateTime, Iterables) -- named arguments -- exporting classes/members to JS -- package:js helper library - - common conversion functions, e.g. jsify/dartify - - predefined interop for core JS types like Set/Map -- extension fields - -Nice to have features (P2): -- helpers for common callback-to-stream patterns -- other helpers like `@JSMethod` -- autoboxing to implement Dart interfaces(?) - -Features for building "package:html" (P2?): -- virtual dispatch for JS classes -- subtyping Dart classes from JS -- subtyping JS classes from Dart -- interface dispatch for JS classes (useful for migrating to package:html) -- dynamic dispatch for JS classes (useful for migrating to package:html) - - -## Compatibility and Evolution - -Most of the features described here are backwards compatible, relative to the -set of JS interop supported by both web compilers. - -One question that came up is interfaces: currently JS interop classes can be -implemented by Dart classes (although this may lead to inconsistent behavior -between the compilers). If `@sealed` Dart members are added to a JS interop -class, then it won't be legal to implement that as an interface. But this is a -new feature, so it's only "breaking" in the sense that adding `@sealed` members -is a breaking change for the package API. (Technically: adding *any* public -member to a Dart class is a breaking change, for this reason. In practice many -types are not intended to be used as interfaces, so they don't treat new members -as breaking). - -If we discover something that is too backwards incompatible, we can add new -annotations and leave the current behavior for the existing ones. Or we can -bump the major version on "package:js". There's a lot of possibilities. - - -## FAQ - -### Q: How does JSON work? - -A: If the JSON is relatively typed, then it can be - - -### Q: Would new Dart language features help? - -A: Yes! All of these features would be helpful: -- extension methods/members -- extension types -- implicit conversions -- autoboxing -- non-null -- generalized typedefs -- `external` on fields (sugar for external getter/setter) - -In the meantime, this proposal provides a way to improve interop, and is -intended to be compatible with those features. - - -### Q: Can dart2js and dartdevc share implementation for JS interop? - -A: Probably, especially for the static features. - -If both compilers are exclusively using Kernel, much of this can -be implemented as a Kernel transform. The static checking can also be done in -a shared way. - -Dynamic dispatch, calling conventions, and runtime type checks, and native SDK -types are different between the two compilers, so those details would require -separate work. - - -### Q: If a JS API returns "Object" does this break dart2js tree shaking? - -A: It does not. Dart classes can be tree-shaken, unless they're explicitly -exported to JS. Static dispatch members can be tree shaken. Static JS Interop -types, by design, do not require any runtime type information (RTTI). -Opt-in features like interface or dynamic dispatch will cause more RTTI to be -retained. - -This question may be referring to "dart:html". It supports dynamic dispatch, and -this causes RTTI to be retained, unless great care is taken on the part -of dart:html authors (to carefully annotate return types, and avoid untyped -results) and by developers (to carefully avoid dynamic calls that use names in -"dart:html", and to avoid JS interop features that could return HTML elements). - -We may be able to find a better solution, such a providing new "package:html" -bindings that are mostly static (and using interface/dynamic dispatch sparingly, -in places where it's really needed). - - -### Q: Could we use dynamic interop instead? - -A: It's certainly possible to imagine interop that works "like JS" -(example from Closure Library's -[goog.editor Example](https://github.com/google/closure-library/blob/master/closure/goog/demos/editor/editor.html#L84)): - -```dart -// Hypothetical dynamic interop -dynamic goog = window.goog; -dynamic myField = goog.editor.Field.new('editMe'); - -// Create and register all of the editing plugins you want to use. -myField.registerPlugin(goog.editor.plugins.BasicTextFormatter.new()); -``` - -The problem is it'll run into the same issues around data type conversions, but -it won't be able to address them without wrapper classes. As illustrated by the -".new" it will never be as simple as copying and pasting JS code, and there -won't be any tooling to help. - -What we may want to do is provide a way to write small chunk of JS code (in a -Dart file), similar to GWT. But that shouldn't be used much, with the -improvements in this proposal to calling conventions and easy ways to access -properties on JS objects. diff --git a/pkg/js/pubspec.yaml b/pkg/js/pubspec.yaml index d8a57feca22..3bc6174f419 100644 --- a/pkg/js/pubspec.yaml +++ b/pkg/js/pubspec.yaml @@ -1,10 +1,10 @@ name: js -version: 0.6.6 +version: 0.6.7 description: Annotations to create static Dart interfaces for JavaScript APIs. repository: https://github.com/dart-lang/sdk/tree/main/pkg/js environment: - sdk: ">=2.19.0-345.0.dev <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: meta: ^1.7.0 diff --git a/pkg/modular_test/test/loader/valid_packages/expectation.txt b/pkg/modular_test/test/loader/valid_packages/expectation.txt index 12742779e66..6d2c19f15d9 100644 --- a/pkg/modular_test/test/loader/valid_packages/expectation.txt +++ b/pkg/modular_test/test/loader/valid_packages/expectation.txt @@ -16,7 +16,6 @@ js dependencies: sdk lib/js.dart lib/js_util.dart - lib/src/varargs.dart main **main module**