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: