Top level await does not work in Meteor 3 rc0

I tried to update an app and stumbled on the same problem that was reported here.

There have been no comments since February - is TLA still targeted to work properly in Meteor 3?

1 Like

From the Git discussion.

import axios from "axios";

const getAndrei = async () => {
  const response = await axios.get('https://api.github.com/users/alisnic')
  return response.data
};

const Andrei = await getAndrei();

export default Andrei;

Something doesnā€™t look right or ā€¦ elegant to say the least. Why not export the function instead of exporting the results?

Created a super simple repro case: GitHub - perbergland/tla-repro: Repro for meteor 3 rc0 top-level await problem

This is the output

=> Started proxy.                             
=> Started MongoDB.                           
I20240503-13:02:00.302(2)? before tla init    
I20240503-13:02:00.306(2)? after tla init
I20240503-13:02:00.306(2)? Starting!
I20240503-13:02:00.306(2)? { tlaValue: undefined, someFunction: undefined }

To me this is very surprising behaviour. I expected TLA to be transparent and that reify would automatically await all imports before continuing loading modules.

That is, this is what I expected:

{ tlaValue: 4, someFunction: (the function code) }

I have added more info to the github issue and got more details from zodern.
A workaround is to add await 0 anywhere in the file that is imported.

Hereā€™s the latest in my ongoing battles to get TLA (top-level await) working for my app. Putting this here instead of Slack so that people can search for it.

Itā€™s quite long, since the issue is complicated.

Background:
My app is written in Typescript and uses the community package refapp:meteor-typescript for the TS=>JS transpilation.

When Meteor 3.0.4 was released last week, I was happy to see that TLA detection was finally fixed. However, when I tested I noticed that it did not work properly in exactly the same way as previously reported in this thread - a simple const x = await someMethod() didnā€™t trigger a module to be loaded as a TLA-marked module.

Since it was reported to be fixed, I filed a bug and resurrected an old repro repo which uses the meteor core ecmascript compiler:

@leonardoventurini was helpful and we had a brief chat in the Slack async channel.

It turned out that for some unknown reason, the published ecmascript package for meteor 3.0.4 includes an old version of babel-compiler which then didnā€™t include the proper fixed reify version.

Missing documentation #1
Compiler packages donā€™t pick up their package and npm dependencies at runtime - they are stored in a pinned mode when a package is published. Like most other aspects of isobuild/meteor-tool, this is not documented anywhere that I have seen.

So then I created a new version of refapp:meteor-typescript published against Meteor 3.0.4 to get the latest version of babel-compiler with the fixed @meteorjs/reify version (included as a dependency on the @meteorjs/babel npm package).
This was done by specifying the meteor release when publishing: meteor publish -release 3.0.4

Success! (or so I thought)
The test module was now marked as async: true (as emitted into .meteor/local/build/programs/server/app/app.js).
But waitā€¦ all modules are marked as async, even the ones that donā€™t have any TLA code.
Whatā€™s going on here? That is not what the code and unit tests say in the GitHub - meteor/reify: Enable ECMAScript 2015 modules in Node today. No caveats. Full stop. repo and for other reify reasons, that was not going to cut it for me.

I needed to find out why reify marked all modules as topLevelAwait and therefore adding async: true instead of async: false into app.js.
To do that, I couldnā€™t think of any other way than to step debug through the reify source that was used when building.

What better way than to use the test apps within the refapp:meteor-typescript repo? I went into the tests/small-typescript-app-meteor3 directory and fired up

TOOL_NODE_FLAGS="--inspect-brk" meteor run

to be able to step through the isobuild/compiler code.

Unfortunately, I couldnā€™t figure which reify source directory that was used so then I couldnā€™t set any breakpoints.

How many copies of reify can there be? Turns out, quite many.
There are 6+ copies of reify 0.25.3 on my machine. I scanned both ~/.meteor and .meteor/local using

find . -type f -path "*/@meteorjs/reify/package.json" -exec sh -c 'echo "File: $1"; jq "{name,version}" < "$1"' _ {} \;

Then I injected console.log calls into some of the reify plugin/babel.js to see which one got executed.

The reify version that was used to compile the test app source code turned out to be:

tests/small-typescript-app-meteor3/.meteor/local/isopacks/refapp_meteor-typescript/plugin.meteor-typescript.os/npm/node_modules/meteor/babel-compiler/node_modules/@meteorjs/reify

It seems that when you compile a dependent package from local sources, the compiled result gets put in .meteor/local/isopacks and executed from there.

By adding console logging in babel.js for some other prime suspects, I could also see that for some other files, the reify version used came from the dev_bundle, i.e.

~/.meteor/packages/meteor-tool/.3.0.4.1tddsze.as7rh++os.osx.arm64+web.browser+web.browser.legacy+web.cordova/mt-os.osx.arm64/dev_bundle/lib/node_modules/@meteorjs/reify/plugins/babel.js

In any case, when I finally figured out which reify version that was used, I could add logging to import-export-visitor.js to figure out what was happening.

Imagine my surprise when I saw the triggering AST:

Node {
  type: 'AwaitExpression',
  start: 142,
  end: 170,
  loc: SourceLocation {
    start: Position { line: 1, column: 142, index: 142 },
    end: Position { line: 1, column: 170, index: 170 },
    filename: undefined,
    identifierName: undefined
  },
  argument: Node {
    type: 'CallExpression',
    start: 148,
    end: 170,
    loc: SourceLocation {
      start: Position { line: 1, column: 148, index: 148 },
      end: Position { line: 1, column: 170, index: 170 },
      filename: undefined,
      identifierName: undefined
    },
    callee: Node {
      type: 'Identifier',
      start: 148,
      end: 168,
      loc: SourceLocation {
        start: Position { line: 1, column: 148, index: 148 },
        end: Position { line: 1, column: 168, index: 168 },
        filename: undefined,
        identifierName: '__reifyWaitForDeps__'
      },
      name: '__reifyWaitForDeps__'
    },
    arguments: []
  },
  extra: { parenthesized: true, parenStart: 141 }
}

Yes, exactly: the triggering AST was the code that reify itself was injecting via babel.

This behavior is only in place when running reify via the babel plugin, and a fix is to add a check for this specific case.

I have created a PR based on this along with a unit test that triggers this bug in the reify repo without the code fix in visitAwaitExpression.

5 Likes

This is excellent, @permb; thank you! :rocket: :metal: