Menci / vite-plugin-top-level-await

Transform code to support top-level await in normal browsers for Vite.
MIT License
273 stars 15 forks source link

declarationToExpression does not correctly re-write functions that change themselves #30

Closed tspink closed 1 year ago

tspink commented 1 year ago

Hello,

I've recently been encountering a serious issue in my project, where Chrome started to report the following error:

Uncaught (in promise) TypeError: Assignment to constant variable.

The specific problem (in my project) is to do with a Babel helper function (_typeof) that reassigns its own definition:

function _typeof(obj) {
    "@babel/helpers - typeof";

    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function (obj) {
            return typeof obj;
        };
    } else {
        _typeof = function (obj) {
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }

    return _typeof(obj);
}

But this only becomes an issue when this particular transform is triggered: https://github.com/Menci/vite-plugin-top-level-await/blob/main/src/transform.ts#L204

Normally, this reassigning would be OK (although not ideal, as I understand it), but that particular transform converts it to something like:

let XX = function _typeof(obj) {
   ...
   _typeof = function () { ... };
    return _typeof(obj);
}

Which causes the "assignment to constant variable" error, since the internal assignment to _typeof (and tail call) has not been re-written to the XX variable. Ideally, it should look like this:

let XX = function _typeof(obj) {
   ...
   XX = function () { ... };
    return XX(obj);
}

I can reproduce this behaviour with a small example:

main.js:

const fooModulePromise = () => import('./foo.js');

await (async function () {
  const fooModule = await fooModulePromise();
  console.log(fooModule.foo(5));
  console.log(fooModule.foo(5));
  console.log(fooModule.foo(5));
})();

foo.js:

function foo(val) {
    return (foo = function (val) { return val * 2 }), val;
}

await (async function test() {
    console.log("test");
})();

export { foo };

vite.config.js:

import { defineConfig } from 'vite'
import topLevelAwait from "vite-plugin-top-level-await";

export default defineConfig({
  build: {
    minify: false,
    sourcemap: true
  },
  plugins: [
    topLevelAwait(),
  ],
})

And, then generated chunk foo-XXX.js:

let foo;
let __tla = (async () => {
  foo = function foo2(val) {
    return foo2 = function(val2) {
      return val2 * 2;
    }, val;
  };
  await async function test() {
    console.log("test");
  }();
})();
export {
  __tla,
  foo
};

Note, the await construct inside foo.js is required to trigger the declarationToExpression transform.

Please let me know if I can provide any more details, or if I can help in any way. Thanks to @nicolo-ribaudo for helping me debug this issue further, when I first reached out to the Babel folk!

Thanks, Tom

Menci commented 1 year ago

Very thanks for providing the detail and investigation!

Menci commented 1 year ago

My plugin generates code that assign the function to a variable with the same name, i.e.:

let foo;
foo = function foo() { foo = /* ... */; };

And the esbuild post-process changes the function name to foo2. But there's no different on the execution result.

Removing the function name resolves this issue. The assignments in the function will assign to the let variable instead the function name, which is read-only. i.e.:

let foo;
foo = function () { foo = /* ... */; };

I found that foo.name is also "foo" even if the function is a anonymous function, so I think there's no breaking in this way.

I'll commit the fix. BTW how do you think of this?

Menci commented 1 year ago

Please try latest v1.3.1.