esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Get a wrong build result when first element of typescript enum is not a Literal value

Open emutime opened this issue 1 year ago • 2 comments

// ModuleDefine.ts
export enum ModuleDefine {
    /** module const space base */
    Base = 10001,
    /** module space offset */
    Offset = 3000,
    /** ModuleBasic */
    ModuleBase = 0,
    /** ModuleA */
    ModuleA,
    /** ModuleB */
    ModuleB,
    /** ModuleC */
    ModuleC
}

// ModuleConstA.ts
import { ModuleDefine } from './ModuleDefine';

export enum ModuleAConst {
    Const1 = ModuleDefine.Base + ModuleDefine.ModuleA * ModuleDefine.Offset,
    Const2,
    Const3
}

in the ModuleConstA.js file, ModuleAConst["Const2"] and ModuleAConst["Const3"] will be transfrom to wrong value "void 0“

// ModuleConstA.js
var ModuleAConst;
var init_ModuleConstA = __esm({
  "ModuleConstA.ts"() {
    init_ModuleDefine();
    ModuleAConst = ((ModuleAConst2) => {
      ModuleAConst2[ModuleAConst2["Const1"] = 10001 /* Base */ + 1 /* ModuleA */ * 3e3 /* Offset */] = "Const1";
      ModuleAConst2[ModuleAConst2["Const2"] = void 0] = "Const2";
      ModuleAConst2[ModuleAConst2["Const3"] = void 0] = "Const3";
      return ModuleAConst2;
    })(ModuleAConst || {});
  }
});

but if i change the Const1 value to 10001 /* Base / + 1 / ModuleA / * 3e3 / Offset */ in ModuleConstA.ts the result is ok

emutime avatar Aug 05 '24 10:08 emutime

Good find! That enum declaration is valid.

https://github.com/evanw/esbuild/blob/9c13ae1f06dfa909eb4a53882e3b7e4216a503fe/internal/js_parser/ts_parser.go#L1381

enum members can be computed as long as they are placed after all constant members. In your case since ModuleDefine.Base + ModuleDefine.ModuleA * ModuleDefine.Offset are all constant members this should be ok

there's a comment in that block

// Whereas in this case:
//
//   enum foo {
//     bar = foo as any,
//   }
//

that is using an invalid enum declaration but i don't think it's related to that.

i think it's related to this info https://esbuild.github.io/content-types/#isolated-modules

also this code here could be the culprit https://github.com/evanw/esbuild/blob/9c13ae1f06dfa909eb4a53882e3b7e4216a503fe/internal/js_parser/ts_parser.go#L1867C18-L1867C50

MauricioAndrades avatar Aug 15 '24 08:08 MauricioAndrades

What esbuild does is the same as what TypeScript does when asked to compile that code as an isolated module:

import { ModuleDefine } from './ModuleDefine';
export var ModuleAConst;
(function (ModuleAConst) {
    ModuleAConst[ModuleAConst["Const1"] = ModuleDefine.Base + ModuleDefine.ModuleA * ModuleDefine.Offset] = "Const1";
    ModuleAConst[ModuleAConst["Const2"] = void 0] = "Const2";
    ModuleAConst[ModuleAConst["Const3"] = void 0] = "Const3";
})(ModuleAConst || (ModuleAConst = {}));

and is also the same as what SWC does:

import { ModuleDefine } from './ModuleDefine';
export var ModuleAConst;
(function(ModuleAConst) {
    ModuleAConst[ModuleAConst["Const1"] = ModuleDefine.Base + ModuleDefine.ModuleA * ModuleDefine.Offset] = "Const1";
    ModuleAConst[ModuleAConst["Const2"] = void 0] = "Const2";
    ModuleAConst[ModuleAConst["Const3"] = void 0] = "Const3";
})(ModuleAConst || (ModuleAConst = {}));

so it seems like esbuild is behaving correctly here. If you think esbuild's output is wrong, then you should work with the TypeScript team to change their output first, in which case esbuild can be updated to match TypeScript's new behavior. I don't think it's appropriate for esbuild to diverge from TypeScript's behavior here as the TypeScript team gets to decide how their language is supposed to work.

evanw avatar Aug 16 '24 19:08 evanw