- Add VariableSpec class with magnitude field validation - Add Formula class supporting multiple input/output variables - Support d4rt_code as string or object with code field - Add comprehensive tests for parsing and serialization - Fix broken test import in pruebas_d4rt_test.dart Follows README.md format requirements exactly
523 lines
19 KiB
Markdown
523 lines
19 KiB
Markdown
[](https://pub.dev/packages/js)
|
||
[](https://pub.dev/packages/js/publisher)
|
||
|
||
> [!CAUTION]
|
||
> This package is discontinued. Prefer using
|
||
> [`dart:js_interop`](https://api.dart.dev/dart-js_interop/dart-js_interop-library.html)
|
||
> for JS interop. See the
|
||
> [JS interop documentation](https://dart.dev/interop/js-interop) for more
|
||
> details.
|
||
|
||
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
|
||
annotations, using them to connect your Dart code with JavaScript.
|
||
|
||
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.
|
||
|
||
[JS interop documentation]: https://dart.dev/interop/js-interop
|
||
|
||
## Static Interop
|
||
|
||
**Important:** Static interop is now supported with extension types in Dart 3.3.
|
||
Prefer using `dart:js_interop` with extension types instead of `@staticInterop`.
|
||
See the [JS interop documentation] for more details.
|
||
|
||
In the past, `package:js` has allowed users to use JavaScript interoperability
|
||
in more dynamic, class-based ways. While we will continue to allow users to use
|
||
that functionality in the foreseeable future, Dart is transitioning to a more
|
||
static, inline class-based interop. What this largely means is that we're moving
|
||
away from dynamic invocations and instead requiring static typing to use
|
||
interop. We're calling this model "static interop".
|
||
|
||
We are doing this for several reasons, such as idiomaticity, performance, type
|
||
soundness, ability to interop with DOM types, and compatibility with Wasm. This
|
||
is an ongoing effort that will affect our web library offerings as well.
|
||
|
||
In version 0.6.6, we introduced a way to use static interop with
|
||
`@staticInterop`. `package:js` classes that have this annotation are required
|
||
to use the new static semantics via extensions and do not support dynamic
|
||
invocations. For more details on how to use `@staticInterop` classes, see below.
|
||
To test these classes, see the sections below on `@JSExport` and
|
||
`js_util.createStaticInteropMock`.
|
||
|
||
As this is an ongoing effort, we are also working on interop with inline
|
||
classes. [Inline classes][] will be a new language feature that enables
|
||
zero-cost wrapping. In the future, users should opt-in to the semantics of
|
||
static interop using inline classes instead of `@staticInterop`. It will be
|
||
easier to use, more idiomatic, and better supported going forward.
|
||
|
||
For now, static interop remains experimental and in development. We may make
|
||
breaking changes in the future. We'll update this text when the new interop
|
||
model matures and is considered stable.
|
||
|
||
[Inline classes]: https://github.com/dart-lang/language/issues/2727
|
||
|
||
## Usage
|
||
|
||
The following examples show how to handle common interoperability tasks.
|
||
|
||
### Calling JavaScript functions
|
||
|
||
```dart
|
||
@JS()
|
||
library stringify;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
// Calls invoke JavaScript `JSON.stringify(obj)`.
|
||
@JS('JSON.stringify')
|
||
external String stringify(Object obj);
|
||
```
|
||
|
||
### Using JavaScript namespaces and classes
|
||
|
||
```dart
|
||
@JS('google.maps')
|
||
library maps;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
// Invokes the JavaScript getter `google.maps.map`.
|
||
external Map get map;
|
||
|
||
// The `Map` constructor invokes JavaScript `new google.maps.Map(location)`
|
||
@JS()
|
||
class Map {
|
||
external Map(Location location);
|
||
external Location getLocation();
|
||
}
|
||
|
||
// The `Location` constructor invokes JavaScript `new google.maps.LatLng(...)`
|
||
//
|
||
// We recommend against using custom JavaScript names whenever
|
||
// possible. It is easier for users if the JavaScript names and Dart names
|
||
// are consistent.
|
||
@JS('LatLng')
|
||
class Location {
|
||
external Location(num lat, num lng);
|
||
}
|
||
```
|
||
|
||
### Passing object literals to JavaScript
|
||
|
||
Many JavaScript APIs take an object literal as an argument. For example:
|
||
|
||
```js
|
||
// JavaScript
|
||
printOptions({ responsive: true });
|
||
```
|
||
|
||
If you want to use `printOptions` from Dart a `Map<String, dynamic>` would be
|
||
"opaque" in JavaScript.
|
||
|
||
Instead, create a Dart class with both the `@JS()` and `@anonymous` annotations.
|
||
|
||
```dart
|
||
@JS()
|
||
library print_options;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
void main() {
|
||
printOptions(Options(responsive: true));
|
||
}
|
||
|
||
@JS()
|
||
external printOptions(Options options);
|
||
|
||
@JS()
|
||
@anonymous
|
||
class Options {
|
||
external bool get responsive;
|
||
|
||
// Must have an unnamed factory constructor with named arguments.
|
||
external factory Options({bool responsive});
|
||
}
|
||
```
|
||
|
||
### 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()`.
|
||
|
||
To make a Dart function callable from JavaScript _by name_, use a setter
|
||
annotated with `@JS()`.
|
||
|
||
```dart
|
||
@JS()
|
||
library callable_function;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
/// Allows assigning a function to be callable from `window.functionName()`
|
||
@JS('functionName')
|
||
external set _functionName(void Function() f);
|
||
|
||
/// Allows calling the assigned function from Dart as well.
|
||
@JS()
|
||
external void functionName();
|
||
|
||
void _someDartFunction() {
|
||
print('Hello from Dart!');
|
||
}
|
||
|
||
void main() {
|
||
_functionName = allowInterop(_someDartFunction);
|
||
// JavaScript code may now call `functionName()` or `window.functionName()`.
|
||
}
|
||
```
|
||
|
||
### @staticInterop
|
||
|
||
With `package:js`, we have historically had two different types of classes:
|
||
plain `@JS` (those with just the `@JS` annotation) and `@anonymous` classes.
|
||
Now, you can use a new one: `@staticInterop`.
|
||
|
||
These classes are different in that they do not allow instance members within
|
||
the class itself. All such members need to go into an extension (hence
|
||
“static”). Let’s look at an example:
|
||
|
||
```dart
|
||
@JS()
|
||
library static_interop;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
// Assumes there is a top-level `StaticInterop` class in a JS module.
|
||
@JS()
|
||
@staticInterop
|
||
class StaticInterop {
|
||
external factory StaticInterop();
|
||
}
|
||
|
||
extension on StaticInterop {
|
||
external int field;
|
||
external int get getSet;
|
||
external set getSet(int val);
|
||
external int method();
|
||
}
|
||
|
||
void main() {
|
||
var jsObj = StaticInterop();
|
||
jsObj.field = 1;
|
||
jsObj.method();
|
||
}
|
||
```
|
||
|
||
The `external` static extension members get lowered to JS naturally:
|
||
`jsObj.field` becomes a property get of `field` in JS and `jsObj.method()`
|
||
becomes a function invocation of `method` on `jsObj`.
|
||
|
||
In many ways, these classes are just like the plain `@JS` and `@anonymous`
|
||
classes. Like with plain `@JS` classes, you can provide a value in `@JS` if you
|
||
want the constructor to use a particular JS class e.g.
|
||
`@JS(‘module.MyJSClass’)`. You can also add `@anonymous` to `@staticInterop`
|
||
classes if you want the factory constructor with named arguments in order to
|
||
make an object literal e.g.
|
||
`external factory AnonymousStaticInterop({int? field1, int? field2})`. Also like
|
||
with plain `@JS` classes, you can’t inherit non-`package:js` classes. You should
|
||
only inherit other `@staticInterop` classes for subtyping and inheriting
|
||
extension methods. Lastly, you can freely cast JS objects to and from the three
|
||
types of `package:js` classes.
|
||
|
||
What makes `@staticInterop` unique, however, is that you can use them to
|
||
represent DOM objects as well as other JS objects, which you can’t with previous
|
||
`package:js` classes. Historically, you’ve needed to use `dart:html` to interact
|
||
with the DOM e.g. `DivElement`. Now, you can create your own abstraction for
|
||
these objects instead of using the ones we provide in `dart:html`:
|
||
|
||
```dart
|
||
@JS()
|
||
library static_interop;
|
||
|
||
import 'dart:html' as html;
|
||
|
||
import 'package:js/js.dart';
|
||
|
||
@JS()
|
||
@staticInterop
|
||
class JSWindow {}
|
||
|
||
extension JSWindowExtension on JSWindow {
|
||
external String get name;
|
||
String get nameAllCaps => name.toUpperCase();
|
||
}
|
||
|
||
void main() {
|
||
var jsWindow = html.window as JSWindow;
|
||
print(jsWindow.name.toUpperCase() == jsWindow.nameAllCaps);
|
||
}
|
||
```
|
||
|
||
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
|
||
- Allow non-`external` members
|
||
- 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.
|
||
|
||
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
|
||
|
||
One of the difficulties with JS interop is that most of it is exclusively
|
||
focused on importing JS code to Dart, not the other way around. We have some
|
||
functionality like `allowInterop`, which allows you to call Dart functions in
|
||
JS, but this becomes cumbersome when you want to use a Dart object. You need to
|
||
essentially `allowInterop` all members manually.
|
||
|
||
`createDartExport` instead lets you do this automatically. Let’s see how with an
|
||
example:
|
||
|
||
```dart
|
||
import 'dart:js_util';
|
||
|
||
import 'package:expect/expect.dart';
|
||
import 'package:js/js.dart';
|
||
|
||
// The Dart class must have `@JSExport` on it or one of its instance members.
|
||
@JSExport()
|
||
class Counter {
|
||
int value = 0;
|
||
@JSExport('increment')
|
||
void renamedIncrement() {
|
||
value++;
|
||
}
|
||
}
|
||
|
||
@JS()
|
||
@staticInterop
|
||
class JSCounter {}
|
||
|
||
extension on JSCounter {
|
||
external int value;
|
||
external void increment();
|
||
}
|
||
|
||
void main() {
|
||
var dartCounter = Counter();
|
||
var counter = createDartExport<Counter>(dartCounter) as JSCounter;
|
||
Expect.equals(0, counter.value);
|
||
counter.increment();
|
||
Expect.equals(1, counter.value);
|
||
Expect.equals(1, dartCounter.value); // Dart object gets modified
|
||
dartCounter.value = 0;
|
||
Expect.equals(0, counter.value); // Changes in Dart object affect the exported object
|
||
}
|
||
```
|
||
|
||
There are a number of things happening here. At a high level, you pass
|
||
`createDartExport` an instance of some Dart object that has `@JSExport` either
|
||
on it or one of its instance members and the object’s static type if needed.
|
||
Using the static type, we transform the `createDartExport` call into a JS object
|
||
literal that is a mapping from each member’s Dart name (accounting for renames
|
||
using the `@JSExport` annotation) to the member. The JS object essentially wraps
|
||
and acts as a proxy to the exported Dart object.
|
||
|
||
Now, when we use it as a JS object (in this case, using `@staticInterop`), we
|
||
can use the same names to access these members. We can also use the same syntax
|
||
to access these members e.g. `counter.value = 0`. This now gives us an easy to
|
||
do what we wanted before with `allowInterop` for each member.
|
||
|
||
There are, of course, limitations.
|
||
|
||
The only members that are “exported” are concrete instance members i.e. fields,
|
||
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.
|
||
|
||
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
|
||
annotation on the class, and only use it on the members you want. If you need to
|
||
rename members, you can provide the `@JSExport` annotation on that member a
|
||
string value, similar to renaming done via `@JS()`. Inheritance respects the
|
||
individual superclass’ annotations. In other words, if the class of the object
|
||
you want to export has a superclass, but that superclass has no `@JSExport`
|
||
annotation anywhere, none of its superclass’ members are exported.
|
||
|
||
Lastly, different members can’t have the same export name, unless they are a
|
||
getter and setter pair. So, for example, if you have a field and a method and
|
||
one of them is renamed to the other’s name, that’s a conflict:
|
||
|
||
```dart
|
||
@JSExport()
|
||
class DartClass {
|
||
int member = 0;
|
||
@JSExport('member') // Two incompatible members have the same export name.
|
||
void method() {}
|
||
}
|
||
```
|
||
|
||
This holds true with inheritance as well, unless the member is overridden.
|
||
|
||
### js_util.createStaticInteropMock
|
||
|
||
One of the neat things about the above example with `Counter` is we’ve
|
||
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
|
||
`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!
|
||
|
||
One useful feature of the old style of mocking using `implements` is it lets you
|
||
know if you've implemented the needed members. We can't do that with
|
||
`createDartExport`. For example:
|
||
|
||
```dart
|
||
@JSExport()
|
||
class Counter {
|
||
// Where is `value` and `increment`?
|
||
}
|
||
```
|
||
|
||
This would obviously not be a satisfactory mock for `JSCounter`.
|
||
`createDartExport` has no idea what class you're trying to mock, so it can't
|
||
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<JSCounter, Counter>(Counter())`, to
|
||
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
|
||
object literal that wraps the Dart object.
|
||
|
||
You can also use `package:mockito` to do the mocking with this API, by providing
|
||
a generated mocking object from `package:mockito` to `createStaticInteropMock`.
|
||
|
||
There are some corner cases here that are worth noting.
|
||
|
||
It is possible, through the expressiveness of extension methods, to have name
|
||
conflicts like this:
|
||
|
||
```dart
|
||
@JS()
|
||
@staticInterop
|
||
class StaticInterop {}
|
||
|
||
extension A on StaticInterop {
|
||
external Function member;
|
||
}
|
||
|
||
extension B on StaticInterop {
|
||
external void member();
|
||
}
|
||
```
|
||
|
||
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_
|
||
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
|
||
as the JS object you are interfacing. For example, if you want to mock a JS
|
||
`Element`, you’d want the type of the mocking object to also be a `Element` in
|
||
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_
|
||
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!).
|
||
|
||
## Reporting issues
|
||
|
||
Please file bugs and feature requests on the [SDK issue tracker][issues].
|
||
|
||
[issues]: https://goo.gl/j3rzs0
|
||
|
||
## Known limitations and bugs
|
||
|
||
<!-- [TODO: add intro. perhaps move this to another page?] -->
|
||
|
||
### Differences between dart2js and dartdevc
|
||
|
||
Dart's production and development JavaScript compilers use different calling
|
||
conventions and type representation, and therefore have different challenges in
|
||
JavaScript interop. There are currently some known differences in behavior and
|
||
bugs in one or both compilers.
|
||
|
||
#### Dartdevc and dart2js have different representation for Maps
|
||
|
||
Passing a `Map<String, String>` as an argument to a JavaScript function will
|
||
have different behavior depending on the compiler. Calling something like
|
||
`JSON.stringify()` will give different results.
|
||
|
||
**Workaround:** Only pass object literals instead of Maps as arguments. For json
|
||
specifically use `jsonEncode` in Dart rather than a JS alternative.
|
||
|
||
#### Missing validation for anonymous factory constructors in dartdevc
|
||
|
||
When using an `@anonymous` class to create JavaScript object literals dart2js
|
||
will enforce that only named arguments are used, while dartdevc will allow
|
||
positional arguments but may generate incorrect code.
|
||
|
||
**Workaround:** Try builds in both development and release mode to get the full
|
||
scope of static validation.
|
||
|
||
### Common problems
|
||
|
||
Dart and JavaScript have different semantics and common patterns, which makes it
|
||
easy to make some mistakes and difficult for the tools to provide safety. These
|
||
common problems are also known as _sharp edges_.
|
||
|
||
#### Lack of runtime type checking
|
||
|
||
The return types of methods annotated with `@JS()` are not validated at runtime,
|
||
so an incorrect type may "leak" into other Dart code and violate type system
|
||
guarantees. This is not true for `@staticInterop` classes unless the
|
||
`@trustTypes` annotation is used.
|
||
|
||
**Workaround:** For any calls into JavaScript code that are not known to be safe
|
||
in their return values, validate the results manually with `is` checks.
|
||
|
||
#### List instances coming from JavaScript will always be `List<dynamic>`
|
||
|
||
A JavaScript array does not have a reified element type, so an array returned
|
||
from a JavaScript function cannot make guarantees about it's elements without
|
||
inspecting each one. At runtime a check like `result is List` may succeed, while
|
||
`result is List<String>` will always fail.
|
||
|
||
**Workaround:** Use `.cast()` or construct a new `List` to get an instance with
|
||
the expected reified type. For instance if you want a `List<String>` use
|
||
`.cast<String>()` or `List<String>.from`.
|
||
|
||
#### The `JsObject` type from `dart:js` can't be used with `@JS()` annotation
|
||
|
||
`JsObject` and related code in `dart:js` uses a different approach and may not
|
||
be passed as an argument to a method annotated with `@JS()`.
|
||
|
||
**Workaround:** Avoid importing `dart:js` and only use the `package:js` provided
|
||
approach. To handle object literals use `@anonymous` on an `@JS()` annotated
|
||
class.
|
||
|
||
#### `is` checks and `as` casts between JS interop types will always succeed
|
||
|
||
For any two `@JS()` types, with or without `@anonymous`, a check of whether an
|
||
object of one type `is` another type will always return true, regardless of
|
||
whether those two types are in the same prototype chain. Similarly, an explicit
|
||
cast using `as` will also succeed.
|