2025-10-15 10:12:44 +03:00

1133 lines
38 KiB
Go

package jsxtransforms
import (
"maps"
"slices"
"strconv"
"strings"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers"
"github.com/dlclark/regexp2"
)
type JSXTransformer struct {
transformers.Transformer
compilerOptions *core.CompilerOptions
emitResolver printer.EmitResolver
importSpecifier string
filenameDeclaration *ast.Node
utilizedImplicitRuntimeImports map[string]map[string]*ast.Node
inJsxChild bool
currentSourceFile *ast.SourceFile
}
func NewJSXTransformer(opts *transformers.TransformOptions) *transformers.Transformer {
compilerOptions := opts.CompilerOptions
emitContext := opts.Context
tx := &JSXTransformer{
compilerOptions: compilerOptions,
emitResolver: opts.EmitResolver,
}
return tx.NewTransformer(tx.visit, emitContext)
}
func (tx *JSXTransformer) getCurrentFileNameExpression() *ast.Node {
if tx.filenameDeclaration != nil {
return tx.filenameDeclaration.AsVariableDeclaration().Name()
}
d := tx.Factory().NewVariableDeclaration(
tx.Factory().NewUniqueNameEx("_jsxFileName", printer.AutoGenerateOptions{
Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel,
}),
nil,
nil,
tx.Factory().NewStringLiteral(tx.currentSourceFile.FileName()),
)
tx.filenameDeclaration = d
return d.AsVariableDeclaration().Name()
}
func (tx *JSXTransformer) getJsxFactoryCalleePrimitive(isStaticChildren bool) string {
if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
return "jsxDEV"
}
if isStaticChildren {
return "jsxs"
}
return "jsx"
}
func (tx *JSXTransformer) getJsxFactoryCallee(isStaticChildren bool) *ast.Node {
t := tx.getJsxFactoryCalleePrimitive(isStaticChildren)
return tx.getImplicitImportForName(t)
}
func (tx *JSXTransformer) getImplicitJsxFragmentReference() *ast.Node {
return tx.getImplicitImportForName("Fragment")
}
func (tx *JSXTransformer) getImplicitImportForName(name string) *ast.Node {
importSource := tx.importSpecifier
if name != "createElement" {
importSource = ast.GetJSXRuntimeImport(importSource, tx.compilerOptions)
}
existing, ok := tx.utilizedImplicitRuntimeImports[importSource]
if ok {
elem, ok := existing[name]
if ok {
return elem.AsImportSpecifier().Name()
}
} else {
tx.utilizedImplicitRuntimeImports[importSource] = make(map[string]*ast.Node)
}
generatedName := tx.Factory().NewUniqueNameEx("_"+name, printer.AutoGenerateOptions{
Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel | printer.GeneratedIdentifierFlagsAllowNameSubstitution,
})
specifier := tx.Factory().NewImportSpecifier(false, tx.Factory().NewIdentifier(name), generatedName)
tx.emitResolver.SetReferencedImportDeclaration(generatedName, specifier)
tx.utilizedImplicitRuntimeImports[importSource][name] = specifier
return specifier.Name()
}
func (tx *JSXTransformer) setInChild(v bool) {
tx.inJsxChild = v
}
func (tx *JSXTransformer) visit(node *ast.Node) *ast.Node {
if node == nil {
return nil
}
if node.SubtreeFacts()&ast.SubtreeContainsJsx == 0 {
return node
}
switch node.Kind {
case ast.KindSourceFile:
tx.setInChild(false)
return tx.visitSourceFile(node.AsSourceFile())
case ast.KindJsxElement:
return tx.visitJsxElement(node.AsJsxElement())
case ast.KindJsxSelfClosingElement:
return tx.visitJsxSelfClosingElement(node.AsJsxSelfClosingElement())
case ast.KindJsxFragment:
return tx.visitJsxFragment(node.AsJsxFragment())
case ast.KindJsxOpeningElement:
panic("JsxOpeningElement should not be visited, handled in visitJsxElement")
case ast.KindJsxOpeningFragment:
panic("JsxOpeningFragment should not be visited, handled in visitJsxFragment")
case ast.KindJsxText:
tx.setInChild(false)
return tx.visitJsxText(node.AsJsxText())
case ast.KindJsxExpression:
tx.setInChild(false)
return tx.visitJsxExpression(node.AsJsxExpression())
}
tx.setInChild(false)
return tx.Visitor().VisitEachChild(node) // by default, do nothing
}
/**
* The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
*/
func hasKeyAfterPropsSpread(node *ast.Node) bool {
spread := false
opener := node
if node.Kind == ast.KindJsxElement {
opener = node.AsJsxElement().OpeningElement
} // otherwise self-closing
for _, elem := range opener.Attributes().Properties() {
if ast.IsJsxSpreadAttribute(elem) && (!ast.IsObjectLiteralExpression(elem.Expression()) || core.Some(elem.Expression().Properties(), ast.IsSpreadAssignment)) {
spread = true
} else if spread && ast.IsJsxAttribute(elem) && ast.IsIdentifier(elem.Name()) && elem.Name().AsIdentifier().Text == "key" {
return true
}
}
return false
}
func (tx *JSXTransformer) shouldUseCreateElement(node *ast.Node) bool {
return len(tx.importSpecifier) == 0 || hasKeyAfterPropsSpread(node)
}
func insertStatementAfterPrologue[T any](to []*ast.Node, statement *ast.Node, isPrologueDirective func(callee T, node *ast.Node) bool, callee T) []*ast.Node {
if statement == nil {
return to
}
statementIdx := 0
// skip all prologue directives to insert at the correct position
for ; statementIdx < len(to); statementIdx++ {
if !isPrologueDirective(callee, to[statementIdx]) {
break
}
}
return slices.Insert(to, statementIdx, statement)
}
func (tx *JSXTransformer) isAnyPrologueDirective(node *ast.Node) bool {
return ast.IsPrologueDirective(node) || (tx.EmitContext().EmitFlags(node)&printer.EFCustomPrologue != 0)
}
func (tx *JSXTransformer) insertStatementAfterCustomPrologue(to []*ast.Node, statement *ast.Node) []*ast.Node {
return insertStatementAfterPrologue(to, statement, (*JSXTransformer).isAnyPrologueDirective, tx)
}
func sortByImportDeclarationSource(a *ast.Node, b *ast.Node) int {
return stringutil.CompareStringsCaseSensitive(a.AsImportDeclaration().ModuleSpecifier.AsStringLiteral().Text, b.AsImportDeclaration().ModuleSpecifier.AsStringLiteral().Text)
}
func getSpecifierOfRequireCall(s *ast.Node) string {
return s.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration().Initializer.AsCallExpression().Arguments.Nodes[0].AsStringLiteral().Text
}
func sortByRequireSource(a *ast.Node, b *ast.Node) int {
return stringutil.CompareStringsCaseSensitive(getSpecifierOfRequireCall(a), getSpecifierOfRequireCall(b))
}
func sortImportSpecifiers(a *ast.Node, b *ast.Node) int {
res := stringutil.CompareStringsCaseSensitive(a.AsImportSpecifier().PropertyName.Text(), b.AsImportSpecifier().PropertyName.Text())
if res != 0 {
return res
}
return stringutil.CompareStringsCaseSensitive(a.AsImportSpecifier().Name().AsIdentifier().Text, b.AsImportSpecifier().Name().AsIdentifier().Text)
}
func getSortedSpecifiers(m map[string]*ast.Node) []*ast.Node {
res := slices.Collect(maps.Values(m))
slices.SortFunc(res, sortImportSpecifiers)
return res
}
func (tx *JSXTransformer) visitSourceFile(file *ast.SourceFile) *ast.Node {
if file.IsDeclarationFile {
return file.AsNode()
}
tx.currentSourceFile = file
tx.importSpecifier = ast.GetJSXImplicitImportBase(tx.compilerOptions, file)
tx.filenameDeclaration = nil
tx.utilizedImplicitRuntimeImports = make(map[string]map[string]*ast.Node)
visited := tx.Visitor().VisitEachChild(file.AsNode())
tx.EmitContext().AddEmitHelper(visited.AsNode(), tx.EmitContext().ReadEmitHelpers()...)
statements := visited.Statements()
statementsUpdated := false
if tx.filenameDeclaration != nil {
statements = tx.insertStatementAfterCustomPrologue(statements, tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(
ast.NodeFlagsConst,
tx.Factory().NewNodeList([]*ast.Node{tx.filenameDeclaration}),
)))
statementsUpdated = true
}
if len(tx.utilizedImplicitRuntimeImports) > 0 {
// A key difference from strada is that these imports are sorted in corsa, rather than appearing in a use-defined order
if ast.IsExternalModule(file) {
statementsUpdated = true
newStatements := make([]*ast.Node, 0, len(tx.utilizedImplicitRuntimeImports))
for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports {
s := tx.Factory().NewImportDeclaration(
nil,
tx.Factory().NewImportClause(ast.KindUnknown, nil, tx.Factory().NewNamedImports(tx.Factory().NewNodeList(getSortedSpecifiers(importSpecifiersMap)))),
tx.Factory().NewStringLiteral(importSource),
nil,
)
ast.SetParentInChildren(s)
newStatements = append(newStatements, s)
}
slices.SortFunc(newStatements, sortByImportDeclarationSource)
for _, e := range newStatements {
statements = tx.insertStatementAfterCustomPrologue(statements, e)
}
} else if ast.IsExternalOrCommonJSModule(file) {
statementsUpdated = true
newStatements := make([]*ast.Node, 0, len(tx.utilizedImplicitRuntimeImports))
for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports {
sorted := getSortedSpecifiers(importSpecifiersMap)
asBindingElems := make([]*ast.Node, 0, len(sorted))
for _, elem := range sorted {
asBindingElems = append(asBindingElems, tx.Factory().NewBindingElement(nil, elem.AsImportSpecifier().PropertyName, elem.AsImportSpecifier().Name(), nil))
}
s := tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(ast.NodeFlagsConst, tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewVariableDeclaration(
tx.Factory().NewBindingPattern(ast.KindObjectBindingPattern, tx.Factory().NewNodeList(asBindingElems)),
nil,
nil,
tx.Factory().NewCallExpression(tx.Factory().NewIdentifier("require"), nil, nil, tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewStringLiteral(importSource)}), ast.NodeFlagsNone),
)})))
ast.SetParentInChildren(s)
newStatements = append(newStatements, s)
}
slices.SortFunc(newStatements, sortByRequireSource)
for _, e := range newStatements {
statements = tx.insertStatementAfterCustomPrologue(statements, e)
}
} else {
// Do nothing (script file) - consider an error in the checker?
}
}
if statementsUpdated {
visited = tx.Factory().UpdateSourceFile(file, tx.Factory().NewNodeList(statements), file.EndOfFileToken)
}
tx.currentSourceFile = nil
tx.importSpecifier = ""
tx.filenameDeclaration = nil
tx.utilizedImplicitRuntimeImports = nil
return visited
}
func (tx *JSXTransformer) visitJsxElement(element *ast.JsxElement) *ast.Node {
tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
if tx.shouldUseCreateElement(element.AsNode()) {
tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
}
return tagTransform(tx, element.OpeningElement, element.Children, element.AsNode())
}
func (tx *JSXTransformer) visitJsxSelfClosingElement(element *ast.JsxSelfClosingElement) *ast.Node {
tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
if tx.shouldUseCreateElement(element.AsNode()) {
tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
}
return tagTransform(tx, element.AsNode(), nil, element.AsNode())
}
func (tx *JSXTransformer) visitJsxFragment(fragment *ast.JsxFragment) *ast.Node {
tagTransform := (*JSXTransformer).visitJsxOpeningFragmentJSX
if len(tx.importSpecifier) == 0 {
tagTransform = (*JSXTransformer).visitJsxOpeningFragmentCreateElement
}
return tagTransform(tx, fragment.OpeningFragment.AsJsxOpeningFragment(), fragment.Children, fragment.AsNode())
}
func (tx *JSXTransformer) convertJsxChildrenToChildrenPropObject(children []*ast.JsxChild) *ast.Node {
prop := tx.convertJsxChildrenToChildrenPropAssignment(children)
if prop == nil {
return nil
}
return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{prop}), false)
}
func (tx *JSXTransformer) transformJsxChildToExpression(node *ast.Node) *ast.Node {
tx.setInChild(true)
defer tx.setInChild(false)
return tx.Visitor().Visit(node)
}
func (tx *JSXTransformer) convertJsxChildrenToChildrenPropAssignment(children []*ast.JsxChild) *ast.Node {
nonWhitespceChildren := ast.GetSemanticJsxChildren(children)
if len(nonWhitespceChildren) == 1 && (nonWhitespceChildren[0].Kind != ast.KindJsxExpression || nonWhitespceChildren[0].AsJsxExpression().DotDotDotToken == nil) {
result := tx.transformJsxChildToExpression(nonWhitespceChildren[0])
if result == nil {
return nil
}
return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, result)
}
results := make([]*ast.Node, 0, len(nonWhitespceChildren))
for _, child := range nonWhitespceChildren {
res := tx.transformJsxChildToExpression(child)
if res == nil {
continue
}
results = append(results, res)
}
if len(results) == 0 {
return nil
}
return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, tx.Factory().NewArrayLiteralExpression(tx.Factory().NewNodeList(results), false))
}
func (tx *JSXTransformer) getTagName(node *ast.Node) *ast.Node {
if node.Kind == ast.KindJsxElement {
return tx.getTagName(node.AsJsxElement().OpeningElement)
} else if ast.IsJsxOpeningLikeElement(node) {
tagName := node.TagName()
if ast.IsIdentifier(tagName) && scanner.IsIntrinsicJsxName(tagName.Text()) {
return tx.Factory().NewStringLiteral(tagName.Text())
} else if ast.IsJsxNamespacedName(tagName) {
return tx.Factory().NewStringLiteral(tagName.AsJsxNamespacedName().Namespace.Text() + ":" + tagName.AsJsxNamespacedName().Name().Text())
} else {
return createExpressionFromEntityName(tx.Factory(), tagName)
}
} else {
panic("unhandled node kind passed to getTagName: " + node.Kind.String())
}
}
func (tx *JSXTransformer) visitJsxOpeningLikeElementJSX(element *ast.Node, children *ast.NodeList, location *ast.Node) *ast.Node {
tagName := tx.getTagName(element)
var childrenProp *ast.Node
if children != nil && len(children.Nodes) > 0 {
childrenProp = tx.convertJsxChildrenToChildrenPropAssignment(children.Nodes)
}
var keyAttr *ast.Node
attrs := element.Attributes().AsJsxAttributes().Properties.Nodes
for i, p := range attrs {
if p.Kind == ast.KindJsxAttribute && p.AsJsxAttribute().Name() != nil && ast.IsIdentifier(p.AsJsxAttribute().Name()) && p.AsJsxAttribute().Name().AsIdentifier().Text == "key" {
keyAttr = p
attrs = slices.Clone(attrs)
attrs = slices.Delete(attrs, i, i+1)
break
}
}
var object *ast.Node
if len(attrs) > 0 {
object = tx.transformJsxAttributesToObjectProps(attrs, childrenProp)
} else {
objectChildren := []*ast.Node{}
if childrenProp != nil {
objectChildren = append(objectChildren, childrenProp)
}
object = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(objectChildren), false) // When there are no attributes, React wants {}
}
return tx.visitJsxOpeningLikeElementOrFragmentJSX(
tagName,
object,
keyAttr,
children,
location,
)
}
func (tx *JSXTransformer) transformJsxAttributesToObjectProps(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
target := tx.compilerOptions.GetEmitScriptTarget()
if target >= core.ScriptTargetES2018 {
// target has object spreads, can keep as-is
return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(tx.transformJsxAttributesToProps(attrs, childrenProp)), false)
}
return tx.transformJsxAttributesToExpression(attrs, childrenProp)
}
func (tx *JSXTransformer) transformJsxAttributesToExpression(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
expressions := make([]*ast.Expression, 0, 2)
properties := make([]*ast.ObjectLiteralElement, 0, len(attrs))
for _, attr := range attrs {
if ast.IsJsxSpreadAttribute(attr) {
// as an optimization we try to flatten the first level of spread inline object
// as if its props would be passed as JSX attributes
if ast.IsObjectLiteralExpression(attr.Expression()) && !hasProto(attr.Expression().AsObjectLiteralExpression()) {
for _, prop := range attr.Expression().Properties() {
if ast.IsSpreadAssignment(prop) {
expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
expressions = append(expressions, tx.Visitor().Visit(prop.Expression()))
continue
}
properties = append(properties, tx.Visitor().Visit(prop))
}
continue
}
expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
expressions = append(expressions, tx.Visitor().Visit(attr.Expression()))
continue
}
properties = append(properties, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
}
if childrenProp != nil {
properties = append(properties, childrenProp)
}
expressions, _ = tx.combinePropertiesIntoNewExpression(expressions, properties)
if len(expressions) > 0 && !ast.IsObjectLiteralExpression(expressions[0]) {
// We must always emit at least one object literal before a spread attribute
// as the JSX always factory expects a fresh object, so we need to make a copy here
// we also avoid mutating an external reference by doing this (first expression is used as assign's target)
expressions = append([]*ast.Expression{tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)}, expressions...)
}
if len(expressions) == 1 {
return expressions[0]
}
return tx.Factory().NewAssignHelper(expressions, tx.compilerOptions.GetEmitScriptTarget())
}
func (tx *JSXTransformer) combinePropertiesIntoNewExpression(expressions []*ast.Expression, props []*ast.ObjectLiteralElement) ([]*ast.Expression, []*ast.ObjectLiteralElement) {
if len(props) == 0 {
return expressions, props
}
newObj := tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(props), false)
expressions = append(expressions, newObj)
return expressions, nil
}
func (tx *JSXTransformer) transformJsxAttributesToProps(attrs []*ast.Node, childrenProp *ast.Node) []*ast.Node {
props := make([]*ast.Node, 0, len(attrs))
for _, attr := range attrs {
if attr.Kind == ast.KindJsxSpreadAttribute {
res := tx.transformJsxSpreadAttributesToProps(attr.AsJsxSpreadAttribute())
props = append(props, res...)
} else {
props = append(props, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
}
}
if childrenProp != nil {
props = append(props, childrenProp)
}
return props
}
func hasProto(obj *ast.ObjectLiteralExpression) bool {
for _, p := range obj.Properties.Nodes {
if ast.IsPropertyAssignment(p) && (ast.IsStringLiteral(p.Name()) || ast.IsIdentifier(p.Name())) && p.Name().Text() == "__proto__" {
return true
}
}
return false
}
func (tx *JSXTransformer) transformJsxSpreadAttributesToProps(node *ast.JsxSpreadAttribute) []*ast.Node {
if ast.IsObjectLiteralExpression(node.Expression) && !hasProto(node.Expression.AsObjectLiteralExpression()) {
res, _ := tx.Visitor().VisitSlice(node.Expression.Properties())
return res
}
return []*ast.Node{tx.Factory().NewSpreadAssignment(tx.Visitor().Visit(node.Expression))}
}
func (tx *JSXTransformer) transformJsxAttributeToObjectLiteralElement(node *ast.JsxAttribute) *ast.Node {
name := tx.getAttributeName(node)
expression := tx.transformJsxAttributeInitializer(node.Initializer)
return tx.Factory().NewPropertyAssignment(nil, name, nil, nil, expression)
}
/**
* Emit an attribute name, which is quoted if it needs to be quoted. Because
* these emit into an object literal property name, we don't need to be worried
* about keywords, just non-identifier characters
*/
func (tx *JSXTransformer) getAttributeName(node *ast.JsxAttribute) *ast.Node {
name := node.Name()
if ast.IsIdentifier(name) {
text := name.Text()
if scanner.IsIdentifierText(text, core.LanguageVariantStandard) {
return name
}
return tx.Factory().NewStringLiteral(text)
}
// must be jsx namespace
return tx.Factory().NewStringLiteral(name.AsJsxNamespacedName().Namespace.Text() + ":" + name.AsJsxNamespacedName().Name().Text())
}
func (tx *JSXTransformer) transformJsxAttributeInitializer(node *ast.Node) *ast.Node {
if node == nil {
return tx.Factory().NewTrueExpression()
}
if node.Kind == ast.KindStringLiteral {
// Always recreate the literal to escape any escape sequences or newlines which may be in the original jsx string and which
// Need to be escaped to be handled correctly in a normal string
res := tx.Factory().NewStringLiteral(decodeEntities(node.Text()))
res.Loc = node.Loc
return res
}
if node.Kind == ast.KindJsxExpression {
if node.AsJsxExpression().Expression == nil {
return tx.Factory().NewTrueExpression()
}
return tx.Visitor().Visit(node.AsJsxExpression().Expression)
}
if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) || ast.IsJsxFragment(node) {
tx.setInChild(false)
return tx.Visitor().Visit(node)
}
panic("Unhandled node kind found in jsx initializer: " + node.Kind.String())
}
func (tx *JSXTransformer) visitJsxOpeningLikeElementOrFragmentJSX(
tagName *ast.Expression,
object *ast.Expression,
keyAttr *ast.Node,
children *ast.NodeList,
location *ast.Node,
) *ast.Node {
var nonWhitespaceChildren []*ast.Node
if children != nil {
nonWhitespaceChildren = ast.GetSemanticJsxChildren(children.Nodes)
}
isStaticChildren := len(nonWhitespaceChildren) > 1 || (len(nonWhitespaceChildren) == 1 && ast.IsJsxExpression(nonWhitespaceChildren[0]) && nonWhitespaceChildren[0].AsJsxExpression().DotDotDotToken != nil)
args := make([]*ast.Node, 0, 3)
args = append(args, tagName, object)
// function jsx(type, config, maybeKey) {}
// "maybeKey" is optional. It is acceptable to use "_jsx" without a third argument
if keyAttr != nil {
args = append(args, tx.transformJsxAttributeInitializer(keyAttr.Initializer()))
}
if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
originalFile := tx.EmitContext().Original(tx.currentSourceFile.AsNode())
if originalFile != nil && ast.IsSourceFile(originalFile) {
// "maybeKey" has to be replaced with "void 0" to not break the jsxDEV signature
if keyAttr == nil {
args = append(args, tx.Factory().NewVoidZeroExpression())
}
// isStaticChildren development flag
if isStaticChildren {
args = append(args, tx.Factory().NewTrueExpression())
} else {
args = append(args, tx.Factory().NewFalseExpression())
}
// __source development flag
line, col := scanner.GetECMALineAndCharacterOfPosition(originalFile.AsSourceFile(), location.Pos())
args = append(args, tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("fileName"), nil, nil, tx.getCurrentFileNameExpression()),
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("lineNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(line+1), 10))),
tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("columnNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(col+1), 10))),
}), false))
// __self development flag
args = append(args, tx.Factory().NewThisExpression())
}
}
element := tx.Factory().NewCallExpression(tx.getJsxFactoryCallee(isStaticChildren), nil, nil, tx.Factory().NewNodeList(args), ast.NodeFlagsNone)
element.Loc = location.Loc
if tx.inJsxChild {
tx.EmitContext().AddEmitFlags(element, printer.EFStartOnNewLine)
}
return element
}
func (tx *JSXTransformer) visitJsxOpeningFragmentJSX(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location *ast.Node) *ast.Node {
var childrenProps *ast.Expression
if children != nil && len(children.Nodes) > 0 {
result := tx.convertJsxChildrenToChildrenPropObject(children.Nodes)
if result != nil {
childrenProps = result
}
}
if childrenProps == nil {
childrenProps = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)
}
return tx.visitJsxOpeningLikeElementOrFragmentJSX(
tx.getImplicitJsxFragmentReference(),
childrenProps,
nil,
children,
location,
)
}
func (tx *JSXTransformer) createReactNamespace(reactNamespace string, parent *ast.Node) *ast.Node {
// To ensure the emit resolver can properly resolve the namespace, we need to
// treat this identifier as if it were a source tree node by clearing the `Synthesized`
// flag and setting a parent node. TODO: Is this still true? The emit resolver is supposed to be
// hardened aginast this, so long as the node retains original node pointers back to a parsed node
if len(reactNamespace) == 0 {
reactNamespace = "React"
}
react := tx.Factory().NewIdentifier(reactNamespace)
react.Flags &= ^ast.NodeFlagsSynthesized
// Set the parent that is in parse tree
// this makes sure that parent chain is intact for checker to traverse complete scope tree
react.Parent = tx.EmitContext().ParseNode(parent)
return react
}
func (tx *JSXTransformer) createJsxFactoryExpressionFromEntityName(e *ast.Node, parent *ast.Node) *ast.Node {
if ast.IsQualifiedName(e) {
left := tx.createJsxFactoryExpressionFromEntityName(e.AsQualifiedName().Left, parent)
right := tx.Factory().NewIdentifier(e.AsQualifiedName().Right.Text())
return tx.Factory().NewPropertyAccessExpression(left, nil, right, ast.NodeFlagsNone)
}
return tx.createReactNamespace(e.AsIdentifier().Text, parent)
}
func (tx *JSXTransformer) createJsxPsuedoFactoryExpression(parent *ast.Node, e *ast.Node, target string) *ast.Node {
if e != nil {
return tx.createJsxFactoryExpressionFromEntityName(e, parent)
}
return tx.Factory().NewPropertyAccessExpression(
tx.createReactNamespace(tx.compilerOptions.ReactNamespace, parent),
nil,
tx.Factory().NewIdentifier(target),
ast.NodeFlagsNone,
)
}
func (tx *JSXTransformer) createJsxFactoryExpression(parent *ast.Node) *ast.Node {
e := tx.emitResolver.GetJsxFactoryEntity(tx.currentSourceFile.AsNode())
return tx.createJsxPsuedoFactoryExpression(parent, e, "createElement")
}
func (tx *JSXTransformer) createJsxFragmentFactoryExpression(parent *ast.Node) *ast.Node {
e := tx.emitResolver.GetJsxFragmentFactoryEntity(tx.currentSourceFile.AsNode())
return tx.createJsxPsuedoFactoryExpression(parent, e, "Fragment")
}
func (tx *JSXTransformer) visitJsxOpeningLikeElementCreateElement(element *ast.Node, children *ast.NodeList, location *ast.Node) *ast.Node {
tagName := tx.getTagName(element)
attrs := element.Attributes().Properties()
var objectProperties *ast.Expression
if len(attrs) > 0 {
objectProperties = tx.transformJsxAttributesToObjectProps(attrs, nil)
} else {
objectProperties = tx.Factory().NewKeywordExpression(ast.KindNullKeyword) // When there are no attributes, React wants "null"
}
var callee *ast.Expression
if len(tx.importSpecifier) == 0 {
callee = tx.createJsxFactoryExpression(element)
} else {
callee = tx.getImplicitImportForName("createElement")
}
var newChildren []*ast.Node
if children != nil && len(children.Nodes) > 0 {
for _, c := range children.Nodes {
res := tx.transformJsxChildToExpression(c)
if res != nil {
if len(children.Nodes) > 1 {
tx.EmitContext().AddEmitFlags(res, printer.EFStartOnNewLine)
}
newChildren = append(newChildren, res)
}
}
}
args := make([]*ast.Expression, 0, len(newChildren)+2)
args = append(args, tagName)
args = append(args, objectProperties)
args = append(args, newChildren...)
result := tx.Factory().NewCallExpression(
callee,
nil,
nil,
tx.Factory().NewNodeList(args),
ast.NodeFlagsNone,
)
result.Loc = location.Loc
if tx.inJsxChild {
tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
}
return result
}
func (tx *JSXTransformer) visitJsxOpeningFragmentCreateElement(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location *ast.Node) *ast.Node {
tagName := tx.createJsxFragmentFactoryExpression(fragment.AsNode())
callee := tx.createJsxFactoryExpression(fragment.AsNode())
var newChildren []*ast.Node
if children != nil && len(children.Nodes) > 0 {
for _, c := range children.Nodes {
res := tx.transformJsxChildToExpression(c)
if res != nil {
if len(children.Nodes) > 1 {
tx.EmitContext().AddEmitFlags(res, printer.EFStartOnNewLine)
}
newChildren = append(newChildren, res)
}
}
}
args := make([]*ast.Expression, 0, len(newChildren)+2)
args = append(args, tagName)
args = append(args, tx.Factory().NewKeywordExpression(ast.KindNullKeyword))
args = append(args, newChildren...)
result := tx.Factory().NewCallExpression(
callee,
nil,
nil,
tx.Factory().NewNodeList(args),
ast.NodeFlagsNone,
)
result.Loc = location.Loc
if tx.inJsxChild {
tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
}
return result
}
func (tx *JSXTransformer) visitJsxText(text *ast.JsxText) *ast.Node {
fixed := fixupWhitespaceAndDecodeEntities(text.Text)
if len(fixed) == 0 {
return nil
}
return tx.Factory().NewStringLiteral(fixed)
}
func addLineOfJsxText(b *strings.Builder, trimmedLine string, isInitial bool) {
// We do not escape the string here as that is handled by the printer
// when it emits the literal. We do, however, need to decode JSX entities.
decoded := decodeEntities(trimmedLine)
if !isInitial {
b.WriteString(" ")
}
b.WriteString(decoded)
}
/**
* JSX trims whitespace at the end and beginning of lines, except that the
* start/end of a tag is considered a start/end of a line only if that line is
* on the same line as the closing tag. See examples in
* tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
* See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
*
* An equivalent algorithm would be:
* - If there is only one line, return it.
* - If there is only whitespace (but multiple lines), return `undefined`.
* - Split the text into lines.
* - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
* - Decode entities on each line (individually).
* - Remove empty lines and join the rest with " ".
*/
func fixupWhitespaceAndDecodeEntities(text string) string {
acc := &strings.Builder{}
initial := true
// First non-whitespace character on this line.
firstNonWhitespace := 0
// End byte position of the last non-whitespace character on this line.
lastNonWhitespaceEnd := -1
// These initial values are special because the first line is:
// firstNonWhitespace = 0 to indicate that we want leading whitespace,
// but lastNonWhitespaceEnd = -1 as a special flag to indicate that we *don't* include the line if it's all whitespace.
for i := 0; i < len(text); i++ {
c, size := utf8.DecodeRuneInString(text[i:])
if stringutil.IsLineBreak(c) {
// If we've seen any non-whitespace characters on this line, add the 'trim' of the line.
// (lastNonWhitespaceEnd === -1 is a special flag to detect whether the first line is all whitespace.)
if firstNonWhitespace != -1 && lastNonWhitespaceEnd != -1 {
addLineOfJsxText(acc, text[firstNonWhitespace:lastNonWhitespaceEnd+1], initial)
initial = false
}
// Reset firstNonWhitespace for the next line.
// Don't bother to reset lastNonWhitespaceEnd because we ignore it if firstNonWhitespace = -1.
firstNonWhitespace = -1
} else if !stringutil.IsWhiteSpaceSingleLine(c) {
lastNonWhitespaceEnd = i + size - 1 // Store the end byte position of the character
if firstNonWhitespace == -1 {
firstNonWhitespace = i
}
}
if size > 1 {
i += (size - 1)
}
}
if firstNonWhitespace != -1 {
// Last line had a non-whitespace character. Emit the 'trimLeft', meaning keep trailing whitespace.
addLineOfJsxText(acc, text[firstNonWhitespace:], initial)
}
return acc.String()
}
func (tx *JSXTransformer) visitJsxExpression(expression *ast.JsxExpression) *ast.Node {
e := tx.Visitor().Visit(expression.Expression)
if expression.DotDotDotToken != nil {
return tx.Factory().NewSpreadElement(e)
}
return e
}
var htmlEntityMatcher = regexp2.MustCompile(`&((#((\d+)|x([\da-fA-F]+)))|(\w+));`, regexp2.ECMAScript)
func htmlEntityReplacer(m regexp2.Match) string {
decimal := m.GroupByNumber(4)
if decimal != nil && decimal.Capture.String() != "" {
parsed, err := strconv.ParseInt(decimal.Capture.String(), 10, 32)
if err == nil {
return string(rune(parsed))
}
}
hex := m.GroupByNumber(5)
if hex != nil && hex.Capture.String() != "" {
parsed, err := strconv.ParseInt(hex.Capture.String(), 16, 32)
if err == nil {
return string(rune(parsed))
}
}
word := m.GroupByNumber(6)
if word != nil && word.Capture.String() != "" {
res, ok := entities[word.Capture.String()]
if ok {
return string(res)
}
}
return m.String()
}
/**
* Replace entities like "&nbsp;", "&#123;", and "&#xDEADBEEF;" with the characters they encode.
* See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
*/
func decodeEntities(text string) string {
res, err := htmlEntityMatcher.ReplaceFunc(text, htmlEntityReplacer, -1, -1)
if err != nil {
panic(err.Error())
}
return res
}
var entities = map[string]rune{
"quot": 0x0022,
"amp": 0x0026,
"apos": 0x0027,
"lt": 0x003C,
"gt": 0x003E,
"nbsp": 0x00A0,
"iexcl": 0x00A1,
"cent": 0x00A2,
"pound": 0x00A3,
"curren": 0x00A4,
"yen": 0x00A5,
"brvbar": 0x00A6,
"sect": 0x00A7,
"uml": 0x00A8,
"copy": 0x00A9,
"ordf": 0x00AA,
"laquo": 0x00AB,
"not": 0x00AC,
"shy": 0x00AD,
"reg": 0x00AE,
"macr": 0x00AF,
"deg": 0x00B0,
"plusmn": 0x00B1,
"sup2": 0x00B2,
"sup3": 0x00B3,
"acute": 0x00B4,
"micro": 0x00B5,
"para": 0x00B6,
"middot": 0x00B7,
"cedil": 0x00B8,
"sup1": 0x00B9,
"ordm": 0x00BA,
"raquo": 0x00BB,
"frac14": 0x00BC,
"frac12": 0x00BD,
"frac34": 0x00BE,
"iquest": 0x00BF,
"Agrave": 0x00C0,
"Aacute": 0x00C1,
"Acirc": 0x00C2,
"Atilde": 0x00C3,
"Auml": 0x00C4,
"Aring": 0x00C5,
"AElig": 0x00C6,
"Ccedil": 0x00C7,
"Egrave": 0x00C8,
"Eacute": 0x00C9,
"Ecirc": 0x00CA,
"Euml": 0x00CB,
"Igrave": 0x00CC,
"Iacute": 0x00CD,
"Icirc": 0x00CE,
"Iuml": 0x00CF,
"ETH": 0x00D0,
"Ntilde": 0x00D1,
"Ograve": 0x00D2,
"Oacute": 0x00D3,
"Ocirc": 0x00D4,
"Otilde": 0x00D5,
"Ouml": 0x00D6,
"times": 0x00D7,
"Oslash": 0x00D8,
"Ugrave": 0x00D9,
"Uacute": 0x00DA,
"Ucirc": 0x00DB,
"Uuml": 0x00DC,
"Yacute": 0x00DD,
"THORN": 0x00DE,
"szlig": 0x00DF,
"agrave": 0x00E0,
"aacute": 0x00E1,
"acirc": 0x00E2,
"atilde": 0x00E3,
"auml": 0x00E4,
"aring": 0x00E5,
"aelig": 0x00E6,
"ccedil": 0x00E7,
"egrave": 0x00E8,
"eacute": 0x00E9,
"ecirc": 0x00EA,
"euml": 0x00EB,
"igrave": 0x00EC,
"iacute": 0x00ED,
"icirc": 0x00EE,
"iuml": 0x00EF,
"eth": 0x00F0,
"ntilde": 0x00F1,
"ograve": 0x00F2,
"oacute": 0x00F3,
"ocirc": 0x00F4,
"otilde": 0x00F5,
"ouml": 0x00F6,
"divide": 0x00F7,
"oslash": 0x00F8,
"ugrave": 0x00F9,
"uacute": 0x00FA,
"ucirc": 0x00FB,
"uuml": 0x00FC,
"yacute": 0x00FD,
"thorn": 0x00FE,
"yuml": 0x00FF,
"OElig": 0x0152,
"oelig": 0x0153,
"Scaron": 0x0160,
"scaron": 0x0161,
"Yuml": 0x0178,
"fnof": 0x0192,
"circ": 0x02C6,
"tilde": 0x02DC,
"Alpha": 0x0391,
"Beta": 0x0392,
"Gamma": 0x0393,
"Delta": 0x0394,
"Epsilon": 0x0395,
"Zeta": 0x0396,
"Eta": 0x0397,
"Theta": 0x0398,
"Iota": 0x0399,
"Kappa": 0x039A,
"Lambda": 0x039B,
"Mu": 0x039C,
"Nu": 0x039D,
"Xi": 0x039E,
"Omicron": 0x039F,
"Pi": 0x03A0,
"Rho": 0x03A1,
"Sigma": 0x03A3,
"Tau": 0x03A4,
"Upsilon": 0x03A5,
"Phi": 0x03A6,
"Chi": 0x03A7,
"Psi": 0x03A8,
"Omega": 0x03A9,
"alpha": 0x03B1,
"beta": 0x03B2,
"gamma": 0x03B3,
"delta": 0x03B4,
"epsilon": 0x03B5,
"zeta": 0x03B6,
"eta": 0x03B7,
"theta": 0x03B8,
"iota": 0x03B9,
"kappa": 0x03BA,
"lambda": 0x03BB,
"mu": 0x03BC,
"nu": 0x03BD,
"xi": 0x03BE,
"omicron": 0x03BF,
"pi": 0x03C0,
"rho": 0x03C1,
"sigmaf": 0x03C2,
"sigma": 0x03C3,
"tau": 0x03C4,
"upsilon": 0x03C5,
"phi": 0x03C6,
"chi": 0x03C7,
"psi": 0x03C8,
"omega": 0x03C9,
"thetasym": 0x03D1,
"upsih": 0x03D2,
"piv": 0x03D6,
"ensp": 0x2002,
"emsp": 0x2003,
"thinsp": 0x2009,
"zwnj": 0x200C,
"zwj": 0x200D,
"lrm": 0x200E,
"rlm": 0x200F,
"ndash": 0x2013,
"mdash": 0x2014,
"lsquo": 0x2018,
"rsquo": 0x2019,
"sbquo": 0x201A,
"ldquo": 0x201C,
"rdquo": 0x201D,
"bdquo": 0x201E,
"dagger": 0x2020,
"Dagger": 0x2021,
"bull": 0x2022,
"hellip": 0x2026,
"permil": 0x2030,
"prime": 0x2032,
"Prime": 0x2033,
"lsaquo": 0x2039,
"rsaquo": 0x203A,
"oline": 0x203E,
"frasl": 0x2044,
"euro": 0x20AC,
"image": 0x2111,
"weierp": 0x2118,
"real": 0x211C,
"trade": 0x2122,
"alefsym": 0x2135,
"larr": 0x2190,
"uarr": 0x2191,
"rarr": 0x2192,
"darr": 0x2193,
"harr": 0x2194,
"crarr": 0x21B5,
"lArr": 0x21D0,
"uArr": 0x21D1,
"rArr": 0x21D2,
"dArr": 0x21D3,
"hArr": 0x21D4,
"forall": 0x2200,
"part": 0x2202,
"exist": 0x2203,
"empty": 0x2205,
"nabla": 0x2207,
"isin": 0x2208,
"notin": 0x2209,
"ni": 0x220B,
"prod": 0x220F,
"sum": 0x2211,
"minus": 0x2212,
"lowast": 0x2217,
"radic": 0x221A,
"prop": 0x221D,
"infin": 0x221E,
"ang": 0x2220,
"and": 0x2227,
"or": 0x2228,
"cap": 0x2229,
"cup": 0x222A,
"int": 0x222B,
"there4": 0x2234,
"sim": 0x223C,
"cong": 0x2245,
"asymp": 0x2248,
"ne": 0x2260,
"equiv": 0x2261,
"le": 0x2264,
"ge": 0x2265,
"sub": 0x2282,
"sup": 0x2283,
"nsub": 0x2284,
"sube": 0x2286,
"supe": 0x2287,
"oplus": 0x2295,
"otimes": 0x2297,
"perp": 0x22A5,
"sdot": 0x22C5,
"lceil": 0x2308,
"rceil": 0x2309,
"lfloor": 0x230A,
"rfloor": 0x230B,
"lang": 0x2329,
"rang": 0x232A,
"loz": 0x25CA,
"spades": 0x2660,
"clubs": 0x2663,
"hearts": 0x2665,
"diams": 0x2666,
}