Babel preset-env not loading top-level await syntax for node target

3.7k Views Asked by At

I am trying out the new top-level await syntax to import a module, but Babel is not recognizing the syntax, despite using preset-env, unless I explicitly set the plugin @babel/plugin-syntax-top-level-await. Why do I have to manually specify this plugin? I was under the impression that preset-env would take care of these things automatically?

For context, my settings are as follows:

    presets: [
      [
        '@babel/preset-env',
        {
          debug: true,
          modules: false,
          useBuiltIns: 'usage',
          corejs: 3,
        },
      ],
      '@babel/preset-typescript',
    ],
    // plugins: ['@babel/plugin-syntax-top-level-await'], // Commented b/c I was experimenting
  };

When running yarn run babel myFile.ts, the output and error thrown are:

@babel/preset-env: `DEBUG` option
Using targets:
{
  "node": "13.12"
}
Using modules transform: false
Using plugins:
  proposal-nullish-coalescing-operator { "node":"13.12" }
  proposal-optional-chaining { "node":"13.12" }
  syntax-json-strings { "node":"13.12" }
  syntax-optional-catch-binding { "node":"13.12" }
  syntax-async-generators { "node":"13.12" }
  syntax-object-rest-spread { "node":"13.12" }
  syntax-dynamic-import { "node":"13.12" }
Using polyfills with `usage` option:
SyntaxError: /path/to/project/src/storage/index.ts: Unexpected token, expected ";" (4:6)
  2 | import { AppError } from '../errors';
  3 | 
> 4 | await import('./db/dbDiskMethods');
    |       ^
  5 | 
  6 | const getDbDiskMethods = async () => {
  7 |   return await import('./db/dbDiskMethods');

As a side question, why does preset-env load the 5 syntax plugins shown in the debug output, but skips top-level await syntax plugin?

2

There are 2 best solutions below

0
On BEST ANSWER

This is happening because preset-env only loads accepted proposals. Currently, top level await has not yet been accepted into the language, it's at stage 3.

In general, list of plugins available to preset-env comes from the compat-data package. The list of plugins actually used by preset-env when Babel is run depends on

The OP error was thrown because Babel was run from command line. Had it been run by babel-loader, the "missing" plugin would have been added automatically as mentioned in the second point above.

0
On

To add a few details to the previous answer:

TLDR

In order to get top-level await to work in Node.js: change your file name to *.mjs or add "type": "module" to your package.json.

In-depth explanation

Firstly, top-level-await has moved to "Stage 4" (meaning: "Finished, Shipping"), and thus is now part of the ES standard. Node v14.18 ships with it out of the box (but there are caveats explained below).

Sadly, top-level-await caller semantics are rather complex. Which means it does not "just work", but depends on "load/require/import logic":

  1. CommonJS does not support top-level await. That has several implications; the most important being: Top-level await only works in ESMs (ECMAScript Modules); or in other words: it only works if you use import, and does not work if you use require. More implications are discussed below.
  2. To add to the trouble, top-level await semantics are entirely determined by the module "caller" or "loader". There is no way to just take the code of a single top-level await file and make it "es5-compatible" without changing the files that load/require/import this code. As you can see in its few lines of source code: without a bundler, the top-level await babel plugin actually does not change the code at all, it only sets some options which are used in determining correctness of the code later down the line.
  3. On the plus side, if you use top-level await with a bundler (e.g. Webpack, Rollup etc.), it should work fine, if you babel your code and have included preset-env or the top-level-await plugin. This will be your go-to choice most of the time, especially when you want to run things in the browser. NOTE that many frontend framework "wrappers" (such as create-react-app, @vue/cli or storybook) use webpack under the hood, and thus should work out of the box. preset-env automatically adds the plugin if the code caller (e.g. babel-loader for webpack) allows it.

Top-level await in Node.js

Due to the complicated nature of this syntax, explained above, Node.js does not support top-level await by default. If you try to run node myscript.js, you will get the error message: await is only valid in async functions and the top level bodies of modules. This error message is actually very clear, yet incomprehensible to someone not very familiar with Node.js's complex module systems.

The problem is, that unless you took very specific steps to tell Node that it should treat your file as a "module" (or "ES Module" or ESM), it won't. That is because ESMs are a more recent feature, and are not implicitely compatible with the good old CommonJS modules, which leads to a lot of frustration when people try mixing the two.

The node documentation states:

Node.js will treat the following as ES modules when passed to node as the initial input, or when referenced by import statements within ES module code:

  • Files ending in .mjs.
  • Files ending in .js when the nearest parent package.json file contains a top-level "type" field with a value of "module".
  • Strings passed in as an argument to --eval, or piped to node via STDIN, with the flag --input-type=module.

Thus, the way to get your top-level await going in Node, is by using the .mjs file extension, or setting the type field in your package.json. Then everything will go nice and smoothly until you fall into any of the other traps involving ESMs.