EmitResolver cannot handle JsxOpeningLikeElement and JsxOpeningFragment that didn't originate from the parse tree
itsdouges opened this issue ยท 3 comments
TypeScript Version: 3.7.x-dev.201xxxxx
Search Terms:
Debug Failure. False expression., typescript transformer, jsx element
Code
- clone
https://github.com/madou/untitled-css-in-js-project
- run
git checkout b229dc749e4614bb8d9194c8de340a82f10c8f8a
- run
yarn
- run
yarn test
- notice one test fails
should not blow up when transforming with const
- notice a similar test but with
var
instead ofconst
passes
the node transformation is done here - if i return the original jsx element node then the test passes. but then that defeats the purpose of the transformer.. ๐
the code is essentially transforming
const Component = () => <div css={{ fontSize: '20px' }}>hello world</div>;
to
const Component = () => (
<>
<style>{'.a { font-size: 20px; } '}</style>
<div className="a">hello world</div>
</>
);
Expected behavior:
it works no error thrown
Actual behavior:
"Debug Failure. False expression."
error thrown. also tried with nightly typescript version - same error.
Related Issues:
๐ would love to get this figured out! hoping it's just something i've done wrong. make from this twitter thread https://twitter.com/orta/status/1206139333448732672
hi is @rbuckton going to be working on this? if not is there any pointers you can give me to fix this? ๐
TL;DR:
- ts.createJsxOpeningElement(ts.createIdentifier('style'), [], ts.createJsxAttributes([])), node,
+ ts.setOriginalNode(ts.createJsxOpeningElement(ts.createIdentifier('style'), [], ts.createJsxAttributes([])), node),
- ts.createJsxOpeningFragment(),
+ ts.setOriginalNode(ts.createJsxOpeningFragment(), node),
The issue title should rather be: "EmitResolver cannot handle JsxOpeningLikeElement and JsxOpeningFragment that didn't originate from the parse tree"
Explanation:
I looked into this. The es2015 transform tries to generate a unique name for block-scoped bindings if it has the same name as another binding in the same function scope. If there is any block scoped binding in the file, it tries to look up every Identifier to detect potential collisions. Since var
is not block-scoped, this code path is not taken in your other test.
Finding out whether there's another binding uses resolveName
which relies on Node#parent
to walk up the AST. This is a really bad idea in transforms as transformed nodes don't have a parent
. resolveNameHelper
contains an assertion that expects the function to only exit at a SourceFile
as that's typically the only Node without parent
.
To prevent everything from blowing up, there's a check in getReferencedDeclarationWithCollidingName
to return early if the current Identifier doesn't come from the parse tree (i.e. the original node is not synthesized) and therefore has no parent
.
Unfortunately there's the JSX transform transforming your JSX Elements to JSX-factory calls. This code (createReactNamespace
in factory.ts
) does some trickery to make it look like real nodes from the parse tree. It unsets the Synthesized
flag and sets the parent
property to the original node of the current JsxOpeningLikeElement | JsxOpeningFragment
.
But your JsxOpeningLikeElements and JsxOpeningFragments don't have an original node. Therefore parent
is set to undefined
which trips up resolveNameHelper
as explained above.
hi @ajafff really appreciate you looking into this - thanks!
with this new knowledge for replacing nodes - how would you recommend replacing a node in a typescript transformer the proper way? (let's say I want to change a function declaration to an arrow function) is setOriginalNode
needed to do that every time?
i've been writing up this handbook https://github.com/madou/typescript-transformer-handbook - would love to have all pro tips and how to's etc in it. if you have time to give it a proof read would be great ๐
fix applied, works great thanks mate