-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inline and erase local const enums #534
Conversation
Thanks for the |
Unfortunately this isn't simple to accomplish, which is the reason why I haven't attempted this yet. Here are some of the trickier cases: export namespace N {
export const enum E {
X = 123
}
}
console.log(N.E.X) import {N} from './file' // where "file" is the code written above
console.log(N.E.X) Doing this transformation correctly in all cases requires a type system, and it's a non-goal for esbuild to replicate TypeScript's type system due to the complexity and maintenance overhead. That's why I currently fall back to having There is an approach where you try to do this when you can and fall back to regular enums when you can't. Doing that means detecting whether or not the identifier for the enum is ever used for something other than a known property access. This is doable for a non-exported enum within a single file, and is hard to do (but not impossible) across multiple files. |
Thanks for the detailed description of the problem. I'll try to experiment a bit more over the next few days |
Hey! Sorry for the lack of activity 😟 I rebased the MR on the latest Right now:
The The doubts that I have:
|
Thanks for working on that! We have tons of const enums and that's a lot of dead weight if they aren't "const". Are there many cases other than namespaces that are troublesome? Keeping const enums in namespaces as plain enums could remain a documented limitation. Full support of namespaced enums is out of question anyway because as you noted, namespaces can be aliased and then require full typing to know what is what. On the other hand I believe you can't alias a const enum itself, so that's probably doable.
|
@jods4 I appreciate the support 🙂 As far as I know, the 2 cases in which
In the solution I prepared, in both cases the Only unexported |
I think 2. is doable and is an important use case as enums are often used across files. For example in our codebase we have lots of enums auto-generated in a single file that is just const enums exports and nothing else. I believe that:
This last bullet is not true once a const enum is exported from a namespace, because the namespace itself can be aliased into new variables, parameters, destructured, etc. etc. This means 1. is not doable without full typing, which esbuild is not going to do. |
What you are saying about point 2 makes sense. I got fooled by thinking esbuild does not have access to symbols exported in a dependency module when parsing another file, but that would not make sense when bundling (I hope I am not mistaken here 😄 ) Back to the drawing board with this MR then, I guess 🙂 I'll try to experiment a bit more with erasing exported const enums, but leaving the ones inside namespaces intact. |
I've got some sad news. I give up on inlining const enum values across files. I've been trying and failing to do that for the last 9h and it looks like a major endeavor 😄 Some findings:
All in all, @evanw was definitely right and I underestimated the effort 😅 I managed to (hopefully) implement the easier solution that @evanw suggested in #534 (comment). I will update the description of the PR @evanw This is a small enhancement, as it only provides value for local const enums, but I reckon we still could try to merge it. Let me know what you think |
Hey @evanw, I'm wondering whether you are still interested in this addition to esbuild, as there's been some period of inactivity in the PR. My intent is not to apply any pressure, but just want to be sure if I should be hoping to get this landed any time soon 🙂 |
TS has an odd feature where new members can be added to pre-existing enums:
Notice that no warnings are generated by tsc:
This PR produces:
|
@kzc Oh, that's surprising 😄 Thanks for pointing it out! I'll try to incorporate it into the solution and add a test for this case. I guess it's a matter of not creating a new |
Here's a test case variation using Given:
Expected:
With this PR:
it's fine without minification:
but when run through terser the incorrect annotations produce:
which leads to the incorrect result:
Likewise with esbuild:
|
Side note - not specific to this PR... In my opinion instead of the following esbuild generated output:
this ought to be generated:
or the more compact arrow form:
since it can be dropped altogether if not used:
and still function correctly if used:
|
The suggested output that @kzc suggests seems to be the best approach. In the issue thread, we said that the real issue of not getting rid of the enums was the code that got generated. If the code gen was changed to output the suggested code, and it was removed by rollup, that seems like the perfect solution. |
I've rebased the branch on the latest It turns out that I had to change the symbol merge strategy for enums - it was One behavior that is different from enum Foo {BAR=1, BAZ=2};
console.log(Foo.BAR, Foo.BAZ, Foo.BOO);
enum Foo {BOO=3};
Unfortunately, due to the single-pass parsing nature of esbuild, the following valid code also reports a warning and const enum Foo {BAR=1, BAZ=2};
function falsePositiveWarningInsideFunction() {
console.log(Foo.BAR, Foo.BAZ, Foo.BOO);
}
const enum Foo {BOO=3};
falsePositiveWarningInsideFunction(); This will likely be a problem with any use of an enum above the place of its declaration. On the output proposed by @kzc - yeah, that would do the job. However, esbuild's output is similar to tsc's one, which also defines a variable in a separate statement, and assigns to it in an IIFE. See this TS playground. I agree that having the syntax you proposed when it's possible (probably when there is a single enum declaration, without any enum merging) would be ideal. I won't pursue that in this PR, though I see that the CI for windows failed, but that looks to be related to the failure on |
Also update snapshots
It it failing at the moment
Const enum values will only be inlined in local scope. If a const enum is used in an unexpected way (e.g. in a function call), the whole enum is preserved. Uses built-in mangling to erase const enums
Merged const enums no longer produce warnings when using enum values from an older declaration.
Referencing a const enum member that comes from a later enum results in a warning.
Using a enum value before it is defined in the file leads to a false-positive warning about the value not being defined yet. This stems from the single-pass parsing nature of esbuild.
Yet another rebase on |
While the With the latest PR:
Even latest
Although
Using the pure annotated IIFE approach outlined in #534 (comment) would allow Ideally, this PR would not emit the unused enum IIFEs at all and avoid this issue altogether. |
Thanks for the followup and more suggestions. I have been testing out various approaches now to get the output to match your suggestion @kzc. For the following input files:
I have managed to get the following esbuild's output:
which looks pretty good. I'll need to try some more approaches. Also, the change is quite messy at the moment, so I'll take some time to improve it. However, as it does seem to be a different way to tackle the problem with const enums, I plan on releasing it as a separate PR. Both PRs could be merged independently, as IMO they introduce different ideas. Also, I don't want to get this one unstable. A couple of notes that I have on my mind:
Let me know if I'm mistaken in some point |
That's great.
For applications, probably not. But if esbuild is used to produce library bundles then it would be common for those library packages to be consumed by other bundlers.
If you already know which enums are in use, and they are not exported from the source file, then there's no advantage to emitting them. And if an enum is exported or if the enum identifier itself is used in a non-enumeration way then it would be a good thing if it could be tree shakable if not necessary.
100% agreement on that point.
Not to my knowledge. The esbuild user would not notice the missing unused enum IIFEs. |
Thanks for the answers again. To my mind, the issue about changing the emitted syntax for enums is orthogonal to this PR and the point about inlining const enum values (which was the main point of #128). If you reckon changing the output syntax could have benefits, it deserves a separate issue, that may catch the attention of @evanw who definitely has moree knowledge than me about the possible implications of such a change (and does not seem to be active in this PR anymore, unfortunately). If you create an issue for it, I may chime in with some comments and findings I came across during the experiment yesterday 🙂 I want to keep the discussion in this PR related to the PR and the issue it tries to solve |
Changing the enum code generation was just mentioned as a generalization because this PR was emitting |
@Gelio thanks a lot. i wish we can merge this soon. |
@AlonMiz This PR has not been reviewed by @evanw yet at all, which I believe is a prerequisite to getting it merged. I am waiting for his input (or any other review, for that matter). I don't think there is much for me to do specifically, aside from maybe resolving merge conflicts and remembering the changes. Since there is no clear direction from @evanw, I don't want to spend time on this PR now only to have it be forgotten for a few months and require a revisit in the future |
Looks like #534 (comment) was just implemented and merged: 731f559 |
Nice improvement, and great to see that In my projects we generate tons of enums from server-side model and reference them in other files, so I'm still hoping for basic direct reference simplification, i.e.: // Direct import of an exported enum const
import { E } from "./enums"
console.log(E.One)
// Compiles to:
console.log(1) Hopefully, this simplification will eventually be doable without full type analysis as it only requires to mark exports that are const enums. That still doesn't support more convoluted cases but it'd be good enough as it covers 99% cases for me. |
Closing this PR now that #128 has been fixed. |
The aim of this PR is to partially address #128
This PR changes the behavior of handling const enums:
Changes in this PR:
Detect
const enum
s (IsConst
field onSEnum
)Display errors when non-literals are used in a
const enum
Add warnings when an unknown member of an enum is referenced (this is for both
const
and regular enums)Mark local const enums as
CanBeRemovedIfUnused
andDoesNotAffectTreeShaking
. Combined with calling not callingrecordUsage
on the enum, this lets the existing mangler (I think) erase the declaration if it is not used in any other way.I thought this way is probably the most elegant.
Keep in mind this is one of my first contributions to esbuild, and one of my first lines of go code, so please point out any mistakes/suggestions 😄