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

Why don't CJS modules support "top level await"? #21267

Closed
benjamingr opened this issue Jun 11, 2018 · 53 comments
Closed

Why don't CJS modules support "top level await"? #21267

benjamingr opened this issue Jun 11, 2018 · 53 comments
Labels
discuss Issues opened for discussions and feedbacks. module Issues and PRs related to the module subsystem. promises Issues and PRs related to ECMAScript promises.

Comments

@benjamingr
Copy link
Member

We can support async/await in our CJS modules and allow people to use top level await.

I saw quite a bit of people asking for this but I haven't seen much discussion about it.

Are there any downsides to this?

  • The module returning a promise and us having to wait until the next require to resolve the chain.
  • We need to know postmortem debugging and exceptions will keep working.
  • Ideally modules without top level await would still resolve entirely synchronously.

cc @addaleax @apapirovski @BridgeAR @MylesBorins @petkaantonov @spion @MadaraUchiha (maybe we need a nodejs/promises team?). Also cc @nodejs/modules in case anyone from there has an opinion to contribute :)

@benjamingr benjamingr added module Issues and PRs related to the module subsystem. discuss Issues opened for discussions and feedbacks. promises Issues and PRs related to ECMAScript promises. labels Jun 11, 2018
@benjamingr benjamingr changed the title Why aren't CJS modules async? Why don't CJS modules support "top level await"? Jun 11, 2018
@devsnek
Copy link
Member

devsnek commented Jun 11, 2018

is there any safe way to do this? even the entry module can be required and we can't detect that so it would just break people expecting require to not return a promise.

on a more theoretical level I think features hinged on cjs being wrapped in a function are generally a bad thing

@Fishrock123
Copy link
Contributor

Ideally modules without top level await would still resolve entirely synchronously.

Having this happen in a hidden way will lead to the worst, most confusing debugging scenarios ever.

@tbranyen
Copy link

Is the idea to basically sugar this:

module.exports = new Promise((resolve, reject) => {
  resolve(true);
});

into:

module.exports = await true;

Doesn't seem that out-of-line to me.

@mcollina
Copy link
Member

I do not think there is any safe way to do this for CJS. The only way to achieve this would be to wrap everything with an async function. This means that all modules will return a Promise.

Ideally modules without top level await would still resolve entirely synchronously.

For what it's stated before, this would not be possible. Promises always resolve asynchronously.

I'm -1 for the time being.

@bmeck
Copy link
Member

bmeck commented Jun 11, 2018

How does this idea differ from using an async IIFE when you want async behavior?

module.exports = (async () => { ... })();

@MylesBorins
Copy link
Contributor

Top Level Await is currently targeting the module goal only, so it will not get adopted by default into CJS, and introducing it would likely introduce weird ecosystem fracture.

Outside of this, I've done some experiments with changing our IIFE with an AIIFE... and it broke everything. CJS does expect execution order and that it happens synchronously.

This seems like something better left to userland imho.

@Fishrock123
Copy link
Contributor

Also, I am pretty sure this is impossible to do in a way that doesn't explode everything. Feel free to prove me wrong but I've also done a lot of thinking about this in regards to async-ness with es modules in the past.

@tbranyen
Copy link

tbranyen commented Jun 12, 2018

@Fishrock123 supporting top-level await would require pre-processing the AST to know about them and then wrapping in an async function. If the end user knew deterministically they were producing a promise, how would this be any different than assigning a promise to module.exports? I'm interested in where this breaks down.

@ljharb
Copy link
Member

ljharb commented Jun 12, 2018

@tbranyen it’s a refactoring hazard if merely adding await dramatically changes the api of the exports, becoming a breaking change.

@tbranyen
Copy link

Doesn't it dramatically change any API?

@ljharb
Copy link
Member

ljharb commented Jun 12, 2018

Depending on how TLA is specced. If TLA is only allowed in modules without exports - which is one of the possibilities - then it wouldn’t impact any APIs at all.

@GeoffreyBooth
Copy link
Member

GeoffreyBooth commented Jun 12, 2018

Forgive me if this is off-topic, and I feel like this must’ve been discussed elsewhere (perhaps ad nauseam) but in the discussion around top-level await has there been any thought given to a way to wrap async functions to make them synchronous?

I’m thinking of Meteor’s wrapAsync (which makes a callback-style function, like fs.readFile, synchronous) or its Promise.await (which makes a promise synchronous). For example, to take MDN’s example:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(2000).then(() => console.log('waited 2 seconds')).catch(err => console.error(err));

console.log('hello!');

This prints hello! immediately, then waited 2 seconds after two seconds go by. Straightforward, this is what we’re all familiar with. But in Meteor, which of course cheats by using Fibers, you can do this:

import { Promise } from 'meteor/promise';

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

Promise.await(wait(2000).then(() => console.log('waited 2 seconds')).catch(err => console.error(err)));

console.log('hello!');

This waits two seconds, prints waited 2 seconds, then immediately prints hello!.

As far as I know this isn’t possible in Node today, other than by pulling in Fibers (as Meteor does), which I’m calling a cheat because it’s essentially hacking Node. But might there be a place for Promise.await in Node core? I know it’s a dangerous thing to add, as it encourages a bad practice, but there might be limited use cases where it’s appropriate; just as there are a smattering of valid use cases for fs.readFileSync, even though the core *Sync functions in general are discouraged.

Anyway there’s probably a better thread for this, I only bring it up here because if Promise.await existed, that could be permissible in CommonJS and equivalent to top-level await.

@devsnek
Copy link
Member

devsnek commented Jun 12, 2018

is Promise.await here just

while (promise.state == pending) {
  uv_run(UV_RUN_ONCE);
  runMicrotaskQueue();
}

@benjamingr
Copy link
Member Author

is Promise.await here just

Basically yeah, you'd wait by running the microtick if a module returned a promise and continue the chain when resolve it is resolved. Towards the outside the API would look synchronous and remain the same.

@ljharb
Copy link
Member

ljharb commented Jun 12, 2018

@GeoffreyBooth no, that's not something the language would ever allow - node allows it with C++ modules, but that's not really a pattern we should be emulating.

Separately, TLA wouldn't block, so a blocking API wouldn't be equivalent.

@benjamingr
Copy link
Member Author

To be clear, I am not suggesting we make something that is compatible with TLA, await in CJS top scope is something a lot of users have been asking for regardless and CJS blocks loading other modules already anyway on doing sync I/O for example.

@ljharb
Copy link
Member

ljharb commented Jun 12, 2018

Blocking in sync i/o is not the same thing as blocking in any async action. Also, adding something like that to CJS - if not supported in the browser - might break a ton of common webpack/browserify workflows.

@tbranyen
Copy link

@ljharb I suspect this sort of thing is probably the primary reason why it shouldn't be pursued. Although browserify/webpack could easily introduce tlaify/tla-loader to support this and probably will independently of Node landing it.

@benjamingr
Copy link
Member Author

@ljharb good point about webpack compatibility. + CJS is frozen.

@bmeck
Copy link
Member

bmeck commented Jun 12, 2018

I think it is still good to think about this sort of thing just for thought experiments. In particular I find mutating module.exports to be a problem:

await null;
// any defered action works
setTimeout(() => { module.exports = {ok: true} });

Since the idea would be that require returns a promise, would this return {} since we don't have a clear indicator when {ok: true} is going to replace the current value? Also it means module.exports might not refer to the value in the CJS cache for modules under require.cache[filename].exports unless we do something for the cache to treat these async CJS modules differently.

With promises we won't be able to change the value once we resolve it. On that note, errors also have some questions and in particular what happens on errors:

await null;
throw new Error();

And what happens when you require the module a second time. Does it cause a reload like CJS currently does, or does it treat this a a success since it was able to return a Promise originally.

All of these lingering questions make for tough design decisions and we can probably just let users wrap things in async IIFEs so they can decide on those behaviors. If those behaviors have clear winners and the IIFEs are too burdensome to maintain we can revisit this I think.

@tbranyen
Copy link

@bmeck IMO it'd work the same as this:

// cat test.js
module.exports = new Promise(resolve => {
  setTimeout(() => {
    module.exports = { ok: true };
  });

  resolve();
});
// cat test2.js
const test = require('./test');

console.log(test);

test.then(resp => {
  console.log(resp);
  console.log(require('./test'));

  delete require.cache[require.resolve('./test')];
  console.log(require('./test'));
});
λ node test2.js
Promise { undefined }
undefined
Promise { undefined }
Promise { undefined }

@bmeck
Copy link
Member

bmeck commented Jun 12, 2018

@tbranyen then module.exports/exports is not the exported value of these async modules in your mind since it doesn show up as {}? What is the value that these fulfill to?

@tbranyen
Copy link

tbranyen commented Jun 12, 2018

@bmeck I'm just showing that this thought experiment exists without top-level await. To answer your question:

module.exports = (async () => {
  setTimeout(() => {
    /* We already know this won't work */
    /*module.exports = { ok: true };*/
  });

  // Would be how you set the "exports"
  return await { ok: true };
})();

Top level return already (confusingly) exists in CJS.

Edit: w/ top level await...

return await { ok: true };

@bmeck
Copy link
Member

bmeck commented Jun 12, 2018

@tbranyen thats very different from how I would have thought your example to work, interesting.

@benjamn
Copy link
Contributor

benjamn commented Jun 12, 2018

It surprises me that there have been so many comments on this thread already, but no one has mentioned dependency cycles yet. Then again, someone always has to be the first to bring cycles into any discussion of module systems, so I guess it's my lucky day. 💫😭

In addition to all the other reasons asynchronous CommonJS is a non-starter, there's this:

The CommonJS policy for handling dependency cycles fundamentally assumes synchronous module evaluation

The following argument is fairly subtle, but please bear with me. There are folks commenting on this thread who are literally designing and implementing Node's next module system, so I really sincerely hope they (in particular) will take the time to parse, instantiate, and evaluate this argument.

I too regret the length of this post, in case you were wondering.

What is the CommonJS policy for handling dependency cycles?

If you require a module that is currently suspended on a call to require (for a module that is currently suspended on a call to require)* for the current module, what happens?

In CommonJS, the rules are clear. When you require a module, the module system must

  1. Obtain a reference to the module object by resolving the module identifier string that was passed to require.
  2. If the module object has no module.exports property yet,
    a. First create and assign module.exports.
    b. Begin evaluating the module, and return module.exports when evaluation is done.
  3. If module.exports is already defined,
    a. Return module.exports immediately from require.
    b. Note: evaluation is skipped in this case because the existence of module.exports implies evaluation has already started.

Hopefully this logic is familiar to everyone.

Why does this policy work?

I claim this policy works pretty well for CommonJS, but only because CommonJS module evaluation is synchronous. Without synchronous evaluation, CommonJS has no good options for coping with dependency cycles.

Case 3 above is the problematic one. When module.exports is returned immediately, either module.exports is complete (the module has finished adding properties to it), or module.exports is incomplete (the module will add additional properties to module.exports in the future, or even completely reassign the module.exports property).

If module.exports is complete, great, we did the right thing!

If module.exports is incomplete, returning it immediately exposes the parent module to some amount of risk. What if the parent tries to access properties that have not been exported yet? How can this be a reasonable policy?

Returning immediately is a reasonable policy for CommonJS because there happens to be only one reason why a module you're attempting to require might have started but not finished evaluation. Namely, the module must be suspended on a call to require that ultimately led to evaluation of the current module. In other words, there must be a dependency cycle, and returning early is the most appropriate course of action in that case. The require call we're waiting on is not going to return if we wait any longer, because our waiting is literally delaying the return, so we might as well return module.exports now.

Take a moment to appreciate this subtlety: if there were any other reasons why a module might be suspended in the middle of its evaluation, then we could not assume that a dependency cycle was to blame, so returning module.exports would not necessarily be the right decision. It might make sense to keep waiting, or throw an exception, or something totally different.

Counting reasons is a pretty weird logical exercise, because there's a big difference between zero, one, and more than one reasons. Why is there exactly one reason in this case? Simply put: the only way for a CommonJS module to suspend itself and call out into other code (that might in turn call require) is by calling synchronous functions. Nothing else has the power to delay synchronous module evaluation while also allowing other code to run.

In other words, synchronous evaluation is essential to CommonJS having a reasonable policy for coping with dependency cycles.

What would happen to this policy if CommonJS module evaluation became asynchronous?

By definition, a module whose evaluation is asynchronous can be suspended for any number of reasons in the middle of its evaluation. Here are two such reasons:

  1. It's currently participating in a dependency cycle with other modules.
  2. It's just trying to do some asynchronous work that will complete soon.

In an asynchronous module system, the second possibility is much more common than the first, and the ability to conceal asynchronous work as an implementation detail is a big part of why async modules are attractive in the first place.

However, a module system still needs a policy for what to do when a suspended module gets imported. When it comes to ECMAScript modules, we have some room to invent new policies, but CommonJS does not have that luxury.

If CommonJS continues returning module.exports immediately whenever it encounters a module suspended in mid-evaluation, that's still a good policy for case 1 (if there really is a dependency cycle).

However, if CommonJS returns module.exports immediately in case 2, then that eliminates the entire value of an asynchronous module system, because it cheats the other module out of any chance to conceal asynchronous work.

If CommonJS instead decides to wait for module.exports to be completely populated, regardless of what mechanism it uses for that waiting, the program will deadlock if we happen to be in case 1 (a dependency cycle), because no progress will be made during the waiting period, because any waiting simply delays the other module from populating its exports further.

Why is that no better than synchronous CommonJS?

If CommonJS always returns module.exports immediately when evaluation is suspended, then no module can count on being able to do any asynchronous work before its exports are exposed to other modules, except work that happens before the first asynchronous pause.

This is literally no different from what we have today: synchronous CommonJS modules can synchronously populate their exports objects, and also (if they like) kick off some independent asynchronous work that might eventually modify their exports, but the rest of the module system won't be waiting around for that work to finish.

This is not such a terrible system (CommonJS works!), but making the module system asynchronous does not improve the situation at all. It just makes a false promise to module authors that they will (only sometimes) be able to conceal asynchronous work.

Couldn't we just detect whether there's a dependency cycle somehow?

If the module you're trying to import turns out to be currently suspended in mid-evaluation on an await expression or a function call, the question the module system must answer is:

Will waiting any longer give this module a chance to finish its work?

This question is equivalent to the Halting Problem (as I think @benjamingr noted recently in another thread), so there is no fool-proof way to decide the answer, in principle.

While I am sympathetic to any close approximations that only get the answer wrong in certain easy-to-spot situations, I have not come across any good approximations.

I would challenge all of you to spend some time chipping away at this academically unsolvable problem, because the advent of ECMAScript top-level await makes it even more important that we have some way of programmatically telling the difference (at least sometimes) between dependency cycles that must be broken, and asynchronous work that deserves to continue.

Thanks for making it this far!

@benjamingr
Copy link
Member Author

Just a note - we can support module top level await in CJS in the way Gus suggested in #21267 (comment) .

It does not require asynchronous resolution as in calling code that has made a require call will resolve it synchronously.

@benjamingr
Copy link
Member Author

benjamingr commented Jun 12, 2018

If we could use await anywhere (not just in the body of async functions), then we would essentially have full coroutines

That is not what we are proposing though and:

array.forEach(elem => {
  ... await ...
}))

Wouldn't work (sorry!), it's just top-level and not anywhere else, like ESM top level await in this regard.

(As a side note - I don't consider @ljharb a "gatekeeper" in any way, I consider him a useful source of ideas and a colleague. I have the same consideration to other TC39 members, in general - communication with TC39 has gotten a lot better over the past year. Also, ljharb is contributing a lot to Node regardless of his involvement with standards bodies and I think the opinions he expresses are his own rather than TC39's)

@benjamn
Copy link
Contributor

benjamn commented Jun 12, 2018

We can pick and choose restrictions on where await can appear. I'm just trying to point out that the extreme version of what we're talking about is both intriguing and relatively coherent (lots of other languages have coroutines). I don't mean to distract from what you're proposing, though, so I'll keep the rest of my thoughts on this topic to myself.

@benjamingr
Copy link
Member Author

@benjamn please do feel free to open a new issue about wait functionality (it wouldn't have to be related to promises by the way).


As a side note, I think the important aspect of @ljharb's comment about webpack/browserify is that there is no 'obvious' way for them to implement this (since they can't literally 'block') without doing heavy AST transforms. I think that the idea is definitely worth exploring regardless but it's an important point.

I'll let discussion happen for a few days and then reach out to the webpack team to understand how hard/easy this is to do.

@benjamn
Copy link
Contributor

benjamn commented Jun 12, 2018

To be clear, I share @ljharb's doubts that any of this is feasible for transpilers and/or module systems that do not have access to a native threading system. I was referring to his comment #21267 (comment), where he signaled clear disapproval about adding native fibers/coroutines to JavaScript.

Here's a recent Babel thread where they seem to have decided they will not attempt to implement a transform for top-level await (and I was one of the people arguing against implementing a transform, to be clear). If anyone is going to try, it will have to be the implementors of runtime module systems (webpack, typescript, meteor, etc.).

Also, for what it's worth, I did not intend any negative connotation in the word "gatekeepers," though I think it is entirely fair to refer to TC39, collectively, as keepers of the JavaScript gate, so to speak.

@MylesBorins
Copy link
Contributor

So I don't know if this helps with the conversation at all... but I am explicitly -1 on changing anything in CJS to support TLA. It was an explicit decision to target only the Module goal for TLA. These types of changes are too broad and too fragile.

IMHO TLA being only in the module goal is a very compelling reason to upgrade 😉

@GeoffreyBooth
Copy link
Member

Not to beat on a dead horse, but isn’t there some value in providing a way to wrap an async function to make it synchronous? In the general sense, not specific to the issue of top-level await and CommonJS, though such functionality would solve this particular issue as well (I think; I guess the await wouldn’t truly be “top-level” if it had to be wrapped in a function that then was wrapped by something like Promise.await, but the effect would be the same for CommonJS if I’m not mistaken). I read this thread which I found interesting: https://stackoverflow.com/questions/21819858/how-to-wrap-async-function-calls-into-a-sync-function-in-node-js-or-javascript

@ljharb I agree it shouldn’t be called Promise.await, or be part of the language standard at all, as things that go into the language should work in browsers. This does, however, feel like something that Node should provide so that people don’t need to resort to hacks like Fibers or using while with calls to the Node event loop. Node already lots of synchronous functions that aren’t related to file I/O; require('child_process').execSync comes to mind. Maybe this could be released as require('util').await, since that module already has promisify and this feels like its cousin. Yes, it wouldn’t be usable in browsers, and I know the trend lately has been to move Node closer to browsers, but browsers don’t need to deal with use cases such as how to bottle up async functions at CommonJS import boundaries.

Anyway I don’t feel strongly about this, just trying to propose a solution. I agree with @MylesBorins that it would be nice to give people a reason to upgrade to ESM 😄

@devsnek
Copy link
Member

devsnek commented Jun 13, 2018

@GeoffreyBooth if you want to see what an api like that looks like -> https://github.com/devsnek/syncify

its worth noting there's pretty much no completely safe way to make this. its just not intended to exist.

@benjamingr
Copy link
Member Author

Not to beat on a dead horse, but isn’t there some value in providing a way to wrap an async function to make it synchronous?

Can we please take the discussion of a way to make code "sync" to another issue? Feel free to open a new one.

@benjamingr
Copy link
Member Author

@devsnek

its worth noting there's pretty much no completely safe way to make this. its just not intended to exist.

Can you please elaborate on this? I'm still waiting on #21267 (comment)

Note that I'm not asking "in general", just specifically for the top level await use case.

@devsnek
Copy link
Member

devsnek commented Jun 13, 2018

@benjamingr what I would describe as sorta "undefined behaviour." like sockets will randomly close, microtask timing gets messed up because the loop isn't running to completion, etc. just little random things that add up into this big ball of unstable. for people who opted into TLA, it would be like node had just gotten a bunch of new bugs. I also think but am not positive that because the loop is in c++, (until node 10ish when they moved marking to another thread) v8's gc will be blocked until the promise is resolved. it also greatly slows down the resolution of the promise as it basically has to wait for the fake event loop to proceed some unknown number of times (in my test you'll notice I tested waiting 1000ms as >= because it frequently comes out ~150ms later)

@benjamingr
Copy link
Member Author

@devsnek thanks, that's a lot of motivation for not doing this "head on".

Note that the suggestion here isn't to expose a syncify in general but only do so internally in our module resolution. Not that this matters from a technical point of view (since the user can still block forever if they await a pending promise or just a really long one where they can await more) - just from an expectations PoV.

@benjamingr
Copy link
Member Author

benjamingr commented Jun 13, 2018

Basically, it would let people do:

// temp.js
module.exports = function () {};
// 1.js
const fn = require('./temp');
await fn();

// main
function syncify(fn) {
  delete require.cache['./1.js']; // should be real path
  delete require.cache['./temp.js']; // ditto
  require('./1.js');
}

And expose the capability outside without having to use a third party package. Risky.

@ChALkeR
Copy link
Member

ChALkeR commented Jul 1, 2018

The most common (if not only) real usecase for the top level await with CJS that I could think of is with using it at the entry point which does not export anything.

So, would allowing await in CJS files that do not export anything do? We can ensure that by blocking writes to module.exports and exports.* at all.

I.e., on require('./file.js'):

  1. Try to load it in a regular way.
  2. If and only if it throws an SyntaxError: await is only valid in async function — load it in async wrapper, but do not allow to write to module.exports and exports.*, i.e. throw a sensible error on attempts to export anything from that file. Return {} synchronously, without waiting for the promise to resolve.

That would not be a breaking change (for users who are not monkey-patching require) as it will change the behaviour only in case where it previously threw.

I also think that it would solve dependency cycles issue, as async modules would not be able to export anything at all and would be just scripts.

Debugging should be easy if any attempt to export things via exports or module.exports will throw in files that need top level await to be parsed — so I do not expect this to create hidden and hard-to-trace bugs.

@ljharb
Copy link
Member

ljharb commented Jul 1, 2018

That restriction makes sense iff the same restriction holds in ESM, imo. I suspect it would obviate a number of problems.

@ChALkeR
Copy link
Member

ChALkeR commented Jul 1, 2018

@ljharb, yes, taking the same approach for ESM top level await could be also a good idea. I do not know why would anyone want to export things asynchronously in ESM (i.e. not immediately after loading imports) — that would just overcomplicate things. The usecase that I see for top level await is for using it in entry points i.e. files that do not export things.


That could probably be relaxed a bit, though from «block exports from files that use top level await» to «block exports after the first await»:

  1. Allow writing to exports.*, block only module.exports.
  2. Block module.exports from re-assignment only after the async function is launched, i.e. allow to assign to it synchronously before the first await.
  3. For ESM, allow top level await only after all exports are done. I.e. do not allow export after top level await.

That will allow to write files that export things at the start and then use await at the bottom do launch something asyncrhonous. Or CJS files that have an if block and do either export or await, depending on if they are the entry point or not. Or other things like that.

Not sure if that is needed, though. Imo, it has some potential in creating strange dependency issues, but nothing more than what we already have with asynchronous assignment to exports.*.

@Trott
Copy link
Member

Trott commented Oct 26, 2018

Should this remain open? Or is this a conversation that has run its course and can be closed?

@BridgeAR
Copy link
Member

BridgeAR commented Jan 4, 2020

I am closing this since there was no activity for over one year. Please reopen in case this should be continued to discuss.

@chenzx
Copy link

chenzx commented Apr 7, 2022

What is the difference between CJS and ESM's running env? CJS loads js module from local file system, while ESM needs perform a async network fetch first, so i guess it's the root cause of TLA design.

However, if the points are how to let CJS support async modules via TLA-like extensions, i thought this misled the way. Why not separately build 2 versions of a module? ESM/CJS bundles can both bundled from a single TS source, with some conditionals supported by bundlers...

@btakita
Copy link

btakita commented May 5, 2023

It looks like the deasync library would allow synchronous import of esm modules. How about adding importSync to the cjs api?

@trusktr
Copy link
Contributor

trusktr commented May 27, 2023

deasync is awesome! There's a newer deasync package with modernized code including type definitions and Promise support:

https://www.npmjs.com/package/@kaciras/deasync

const { awaitSync } = require("@kaciras/deasync");

module.exports = awaitSync(import('./some/es/module.js'))

It doesn't seem to work on my Apple silicon though. Seems to lock up when I tested in a VS Code extension.

A built-in solution in Node.js would be really nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss Issues opened for discussions and feedbacks. module Issues and PRs related to the module subsystem. promises Issues and PRs related to ECMAScript promises.
Projects
None yet
Development

No branches or pull requests