From f5336db82ae301f8ac35d69d532270d051a76e81 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 18 Jun 2015 14:01:49 -0700 Subject: [PATCH] Emitter for JSX --- src/compiler/emitter.ts | 325 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 310 insertions(+), 15 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 469b13294ad..265690cd680 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -47,16 +47,18 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } };`; + let compilerOptions = host.getCompilerOptions(); let languageVersion = compilerOptions.target || ScriptTarget.ES3; let sourceMapDataList: SourceMapData[] = compilerOptions.sourceMap || compilerOptions.inlineSourceMap ? [] : undefined; let diagnostics: Diagnostic[] = []; let newLine = host.getNewLine(); + let jsxDesugaring = host.getCompilerOptions().jsx === JsxEmit.React; if (targetSourceFile === undefined) { forEach(host.getSourceFiles(), sourceFile => { if (shouldEmitToOwnFile(sourceFile, compilerOptions)) { - let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, ".js"); + let jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, (!sourceFile.isTSXFile || jsxDesugaring) ? ".js" : ".jsx"); emitFile(jsFilePath, sourceFile); } }); @@ -68,7 +70,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { else { // targetSourceFile is specified (e.g calling emitter from language service or calling getSemanticDiagnostic from language service) if (shouldEmitToOwnFile(targetSourceFile, compilerOptions)) { - let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, ".js"); + let jsFilePath = getOwnEmitOutputFilePath(targetSourceFile, host, (host.getSourceFiles().every(f => !f.isTSXFile) || jsxDesugaring) ? ".js" : ".jsx"); emitFile(jsFilePath, targetSourceFile); } else if (!isDeclarationFile(targetSourceFile) && compilerOptions.out) { @@ -1106,6 +1108,203 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { emit(span.literal); } + function jsxEmitReact(node: JsxElement|JsxSelfClosingElement) { + /// Emit a tag name, which is either '"div"' for lower-cased names, or + /// 'Div' for upper-cased or dotted names + function emitTagName(name: Identifier|QualifiedName) { + if (name.kind === SyntaxKind.Identifier) { + var ch = (name).text.charAt(0); + if (ch.toUpperCase() === ch) { + emit(name); + } + else { + write('"'); + emit(name); + write('"'); + } + return ch.toUpperCase() !== ch; + } + else { + Debug.assert(name.kind === SyntaxKind.QualifiedName); + emit(name); + } + } + + /// 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 + function emitAttributeName(name: Identifier) { + if (/[A-Za-z_]+[\w*]/.test(name.text)) { + write('"'); + emit(name); + write('"'); + } + else { + emit(name); + } + } + + /// Emit an name/value pair for an attribute (e.g. "x: 3") + function emitJsxAttribute(node: JsxAttribute) { + emitAttributeName(node.name); + write(': '); + emit(node.initializer); + } + + function emitJsxElement(openingNode: JsxOpeningElement|JsxSelfClosingElement, children?: JsxChild[]) { + // Call React.createElement(tag, ... + emitLeadingComments(openingNode); + write('React.createElement('); + emitTagName(openingNode.tagName); + write(', '); + + // Attribute list + if (openingNode.attributes.length === 0) { + // When there are no attributes, React wants 'null' + write('null'); + } + else { + // Either emit one big object literal (no spread attribs), or + // a call to React.__spread + let attrs = openingNode.attributes; + if (attrs.some(attr => attr.kind === SyntaxKind.JsxSpreadAttribute)) { + write('React.__spread('); + + let haveOpenedObjectLiteral = false; + for (var i = 0; i < attrs.length; i++) { + if (attrs[i].kind === SyntaxKind.JsxSpreadAttribute) { + if (haveOpenedObjectLiteral) { + write('}'); + haveOpenedObjectLiteral = false; + } + if (i > 0) write(', '); + emit((attrs[i]).expression); + } + else { + Debug.assert(attrs[i].kind === SyntaxKind.JsxAttribute); + if (haveOpenedObjectLiteral) { + write(', '); + } + else { + haveOpenedObjectLiteral = true; + if (i > 0) write(', '); + write('{'); + } + emitJsxAttribute(attrs[i]); + } + } + if (haveOpenedObjectLiteral) write('}'); + + write(')'); // closing paren to React.__spread( + } + else { + // One object literal with all the attributes in them + write('{'); + for (var i = 0; i < attrs.length; i++) { + if (i > 0) write(', '); + emitJsxAttribute(attrs[i]); + } + write('}'); + } + } + + // Children + if (children) { + for (var i = 0; i < children.length; i++) { + // Don't emit empty expressions + if (children[i].kind === SyntaxKind.JsxExpression && !((children[i]).expression)) continue; + + // Don't emit empty strings + if (children[i].kind === SyntaxKind.JsxText && !shouldEmitJsxText(children[i])) continue; + + write(', '); + emit(children[i]); + } + } + + // Closing paren + write(')'); // closes 'React.createElement(' + emitTrailingComments(openingNode); + } + + if (node.kind === SyntaxKind.JsxElement) { + emitJsxElement((node).openingElement, (node).children); + } + else { + Debug.assert(node.kind === SyntaxKind.JsxSelfClosingElement); + emitJsxElement(node); + } + } + + function jsxEmitPreserve(node: JsxElement|JsxSelfClosingElement) { + function emitJsxAttribute(node: JsxAttribute) { + emit(node.name); + write('='); + emit(node.initializer); + } + + function emitJsxSpreadAttribute(node: JsxSpreadAttribute) { + write('{...'); + emit(node.expression); + write('}'); + } + + function emitAttributes(attribs: NodeArray) { + for (var i = 0, n = attribs.length; i < n; i++) { + if (i > 0) write(' '); + + if (attribs[i].kind === SyntaxKind.JsxSpreadAttribute) { + emitJsxSpreadAttribute(attribs[i]); + } + else { + Debug.assert(attribs[i].kind === SyntaxKind.JsxAttribute); + emitJsxAttribute(attribs[i]); + } + } + } + + function emitJsxOpeningOrSelfClosingElement(node: JsxOpeningElement|JsxSelfClosingElement) { + write('<'); + emit(node.tagName); + if (node.attributes.length > 0 || (node.kind === SyntaxKind.JsxSelfClosingElement)) { + write(' '); + } + + emitAttributes(node.attributes); + + if (node.kind === SyntaxKind.JsxSelfClosingElement) { + write('/>'); + } + else { + write('>'); + } + } + + function emitJsxClosingElement(node: JsxClosingElement) { + write(''); + } + + function emitJsxElement(node: JsxElement) { + emitJsxOpeningOrSelfClosingElement(node.openingElement); + + for (var i = 0, n = node.children.length; i < n; i++) { + emit(node.children[i]); + } + + emitJsxClosingElement(node.closingElement); + } + + if (node.kind === SyntaxKind.JsxElement) { + emitJsxElement(node); + } + else { + Debug.assert(node.kind === SyntaxKind.JsxSelfClosingElement); + emitJsxOpeningOrSelfClosingElement(node); + } + } + // This function specifically handles numeric/string literals for enum and accessor 'identifiers'. // In a sense, it does not actually emit identifiers as much as it declares a name for a specific property. // For example, this is utilized when feeding in a result to Object.defineProperty. @@ -1182,6 +1381,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { case SyntaxKind.ForInStatement: case SyntaxKind.ForOfStatement: case SyntaxKind.IfStatement: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxOpeningElement: case SyntaxKind.NewExpression: case SyntaxKind.ParenthesizedExpression: case SyntaxKind.PostfixUnaryExpression: @@ -1658,8 +1859,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { function parenthesizeForAccess(expr: Expression): LeftHandSideExpression { // When diagnosing whether the expression needs parentheses, the decision should be based // on the innermost expression in a chain of nested type assertions. - while (expr.kind === SyntaxKind.TypeAssertionExpression) { - expr = (expr).expression; + while (expr.kind === SyntaxKind.TypeAssertionExpression || expr.kind === SyntaxKind.AsExpression) { + expr = (expr).expression; } // isLeftHandSideExpression is almost the correct criterion for when it is not necessary @@ -1805,8 +2006,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { } function skipParentheses(node: Expression): Expression { - while (node.kind === SyntaxKind.ParenthesizedExpression || node.kind === SyntaxKind.TypeAssertionExpression) { - node = (node).expression; + while (node.kind === SyntaxKind.ParenthesizedExpression || node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression) { + node = (node).expression; } return node; } @@ -1956,12 +2157,12 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { // not the user. If we didn't want them, the emitter would not have put them // there. if (!nodeIsSynthesized(node) && node.parent.kind !== SyntaxKind.ArrowFunction) { - if (node.expression.kind === SyntaxKind.TypeAssertionExpression) { + if (node.expression.kind === SyntaxKind.TypeAssertionExpression || node.expression.kind === SyntaxKind.AsExpression) { let operand = (node.expression).expression; // Make sure we consider all nested cast expressions, e.g.: // (-A).x; - while (operand.kind === SyntaxKind.TypeAssertionExpression) { + while (operand.kind === SyntaxKind.TypeAssertionExpression || operand.kind === SyntaxKind.AsExpression) { operand = (operand).expression; } @@ -3807,7 +4008,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { emitClassLikeDeclarationForES6AndHigher(node); } } - + function emitClassLikeDeclarationForES6AndHigher(node: ClassLikeDeclaration) { let thisNodeIsDecorated = nodeIsDecorated(node); if (node.kind === SyntaxKind.ClassDeclaration) { @@ -4066,7 +4267,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { write(".prototype"); } } - + function emitDecoratorsOfClass(node: ClassLikeDeclaration) { emitDecoratorsOfMembers(node, /*staticFlag*/ 0); emitDecoratorsOfMembers(node, NodeFlags.Static); @@ -4978,7 +5179,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { let started = false; for (let importNode of externalImports) { // do not create variable declaration for exports and imports that lack import clause - let skipNode = + let skipNode = importNode.kind === SyntaxKind.ExportDeclaration || (importNode.kind === SyntaxKind.ImportDeclaration && !(importNode).importClause) @@ -5075,7 +5276,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { write("};"); return emitExportStarFunction(exportedNamesStorageRef); - + function emitExportStarFunction(localNames: string): string { const exportStarFunction = makeUniqueName("exportStar"); @@ -5102,7 +5303,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { return exportStarFunction; } - + function writeExportedName(node: Identifier | Declaration): void { // do not record default exports // they are local to module and never overwritten (explicitly skipped) by star export @@ -5398,7 +5599,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { if (importNode.kind === SyntaxKind.ImportDeclaration && (importNode).importClause.namedBindings) { - + let namedBindings = (importNode).importClause.namedBindings; if (namedBindings.kind === SyntaxKind.NamespaceImport) { // emit re-export for namespace @@ -5657,6 +5858,91 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { } } + function emitJsxElement(node: JsxElement|JsxSelfClosingElement) { + switch (compilerOptions.jsx) { + case JsxEmit.React: + jsxEmitReact(node); + break; + // Fall back to preserve if None was specified (we'll error earlier) + default: + jsxEmitPreserve(node); + break; + } + } + + function trimReactWhitespace(node: JsxText): string { + // Could be empty string, do not use !node.formattedReactText + if (node.formattedReactText !== undefined) { + return node.formattedReactText; + } + + let lines: string[] = []; + let text = getTextOfNode(node); + let firstNonWhitespace = 0; + let lastNonWhitespace = -1; + + for (var i = 0; i < text.length; i++) { + var c = text.charCodeAt(i); + if (c === CharacterCodes.lineFeed || c === CharacterCodes.carriageReturn) { + if (firstNonWhitespace !== -1 && (lastNonWhitespace - firstNonWhitespace + 1 > 0)) { + lines.push(text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1)); + } + firstNonWhitespace = -1; + } + else if (!isWhiteSpace(c)) { + lastNonWhitespace = i; + if (firstNonWhitespace === -1) { + firstNonWhitespace = i; + } + } + } + if (firstNonWhitespace !== -1) { + lines.push(text.substr(firstNonWhitespace)); + } + + return node.formattedReactText = lines.join('" + \' \' + "'); + } + + function shouldEmitJsxText(node: JsxText) { + if (compilerOptions.jsx === JsxEmit.React) { + return trimReactWhitespace(node).length > 0; + } + else { + return true; + } + } + + function emitJsxText(node: JsxText) { + switch (compilerOptions.jsx) { + case JsxEmit.React: + write('"'); + write(trimReactWhitespace(node)); + write('"'); + break; + + case JsxEmit.Preserve: + default: // Emit JSX-preserve as default when no --jsx flag is specified + write(getTextOfNode(node, true)); + break; + } + } + + function emitJsxExpression(node: JsxExpression) { + if (node.expression && node.expression.kind !== SyntaxKind.OmittedExpression) { + switch (compilerOptions.jsx) { + case JsxEmit.Preserve: + default: + write('{'); + emit(node.expression); + write('}'); + break; + case JsxEmit.React: + emit(node.expression); + break; + } + } + } + function emitDirectivePrologues(statements: Node[], startWithNewLine: boolean): number { for (let i = 0; i < statements.length; ++i) { if (isPrologueDirective(statements[i])) { @@ -5799,7 +6085,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { if (node.kind !== SyntaxKind.Block && node.parent && node.parent.kind === SyntaxKind.ArrowFunction && - (node.parent).body === node && + (node.parent).body === node && compilerOptions.target <= ScriptTarget.ES5) { return false; @@ -5844,6 +6130,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { return emitTemplateExpression(node); case SyntaxKind.TemplateSpan: return emitTemplateSpan(node); + case SyntaxKind.JsxElement: + case SyntaxKind.JsxSelfClosingElement: + return emitJsxElement(node); + case SyntaxKind.JsxText: + return emitJsxText(node); + case SyntaxKind.JsxExpression: + return emitJsxExpression(node); case SyntaxKind.QualifiedName: return emitQualifiedName(node); case SyntaxKind.ObjectBindingPattern: @@ -5874,6 +6167,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) { return emitTaggedTemplateExpression(node); case SyntaxKind.TypeAssertionExpression: return emit((node).expression); + case SyntaxKind.AsExpression: + return emit((node).expression); case SyntaxKind.ParenthesizedExpression: return emitParenExpression(node); case SyntaxKind.FunctionDeclaration: