Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[js_interop] Allow module-scoped exports for top level members #55029

Open
pattobrien opened this issue Feb 27, 2024 · 3 comments
Open

[js_interop] Allow module-scoped exports for top level members #55029

pattobrien opened this issue Feb 27, 2024 · 3 comments
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. web-js-interop Issues that impact all js interop

Comments

@pattobrien
Copy link

pattobrien commented Feb 27, 2024

Problem

There is currently no way to export a function or class to the module scope, rather than the global scope. Module-scoped functions and classes are the defacto ways of sharing code between modules, and slight tweaks to the compilation process would enable js interop within multiple environments beyond the Browser.

Description

I'm working on a node_interop experiment using the latest Dart js_interop library. Though Dart has generally only been concerned with running Dart in browser environments and as the primary client, I've been pleasantly surprised that it was not difficult to get Dart to run on a local machine as a module (i.e. called from another script) using Node.js, and that little work is required to enable such environment-agnostic js compilation.

Part of my node_interop experimentation includes creating a VSCode extension using Dart. VSCode requires defining a module as entrypoint, which is expected to expose an activate and deactivate function. I've managed to get a Dart VSCode extension working by defining the following entrypoint script that calls into the generated dart code.

// [index.js]

// creates a `self` object on preamble, and adds `self` to global scope
const preamble = require('./node_preamble.js'); 

// run dart2js compiled javascript
require('./gen/extension.dart.js'); 

// extract the new exports created by `extension.dart.js`
const generatedExports = preamble.self.exports;

// add to module scope
module.exports = generatedExports;

Dart code:

// [bin/extension.dart]

Future<void> main() async {
  _activate = allowInterop(activateImplementation).toJS;
}

@JS('exports.activate')
external set _activate(JSExportedDartFunction func);

@JS('exports.activate')
external void activate(ExtensionContext context);

void activateImplementation(ExtensionContext context) {
  print('Congratulations, your extension "vscode_interop" is now active!');

  var disposable = commands.registerCommand(
      'vscode_interop.helloWorld',
      () {
        window.showInformationMessage('Hello from Dart!');
      }.toJS);

  context.subscriptions.push(disposable);
}

This separate package:build issue is regarding allowing a preamble to be added to the beginning of the generated js script, while this issue targets the second half of the index.js script. Both solutions together would enable dart2js compilation to multiple environment targets without a separate main script.

A related problem is that the ergonomics around defining a top level function are quite poor. Dart currently requires defining three separate functions: a setter to set the function using allowInterop, the actual implementation (activateImplementation above), and (optionally) the Dart-callable exported function declaration.

Proposed Solution

There are two separate problems with their own proposed solutions:

  • allow exporting to the global module object available in Node.JS runtimes, via a module getter, similar to globalThis
  • allow more elegant exporting of methods or functions

Using a new @JSModuleExport annotation, the above Dart code would be reduced by 50% to the following:

@JSModuleExport()
void activate(ExtensionContext context) {
  print('Congratulations, your extension "vscode_interop" is now active!');

  var disposable = commands.registerCommand(
      'vscode_interop.helloWorld',
      () {
        window.showInformationMessage('Hello from Dart!');
      }.toJS);

  context.subscriptions.push(disposable);
}

or alternatively using extension types:

@JSModuleExport()
extension type activate(JSExportedDartFunction func) implements JSFunction {
  void call(ExtensionContext context) {
    print('Congratulations, your extension "vscode_interop" is now active!');

    var disposable = commands.registerCommand(
        'vscode-interop.helloWorld',
        () {
          window.showInformationMessage('Hello World from Dart!!!');
        }.toJS);

    context.subscriptions.push(disposable);
  }
}

Note the following improvements:

  • functions are exported to the module scope, without the need of a index.js export-extraction script
  • no main() function requirement - the existence of an @export annotation should imply an entrypoint to the compilers
  • remove the pattern of needing to write an external getter, external setter, and concrete implementation
@lrhn lrhn added the area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. label Feb 27, 2024
@sigmundch sigmundch added the web-js-interop Issues that impact all js interop label Feb 27, 2024
@kevmoo

This comment was marked as off-topic.

@srujzs
Copy link
Contributor

srujzs commented Feb 27, 2024

Consuming Dart output as modules is probably not intended but might just work. I think a lot of this depends on if that is something we want to support. cc @sigmundch

IIUC, you're not looking for separating Dart output into multiple modules (one per library) but rather one module. In a sense, this is the inverse of #53783 where instead of consuming modules, you're producing one.

allow exporting to the global module object available in Node.JS runtimes, via a module getter, similar to globalThis

This is an interesting proposal. It seems like module is the node.js global that is specific to that module, so if the Dart output were a usable module, presumably this top-level:

@JS()
external JSObject get module;

could just be that member?

I think having an annotation to tell the compiler to export some functions could work, although we'd need to figure out where in the Dart code the call to export the functions go.

_activate = allowInterop(activateImplementation).toJS;

P.S. you're doing the same operation twice. :) Function.toJS is roughly equivalent to allowInterop - it just produces a separate static type, so you can remove the allowInterop.

@pattobrien
Copy link
Author

@kevmoo awesome! just sent you a DM on twitter :)

IIUC, you're not looking for separating Dart output into multiple modules (one per library) but rather one module.
presumably this top-level could just be that member?

That's right. As long as the js compilers are designed to compile the entire program into a single js file, then a single top-level module getter would be sufficient. I don't see significant pros/cons to going one way or the other (vs. an annotation).

P.S. you're doing the same operation twice. :)

Wow, and I came up with that after spending an embarrassing amount of time getting JSExportedDartFunction to work at all! Thanks for this tip :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-web Use area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop. web-js-interop Issues that impact all js interop
Projects
None yet
Development

No branches or pull requests

5 participants