diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 98cbaca5e6f2c..d78831548cddf 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -207,7 +207,8 @@ namespace ts { type: createMapFromTemplate({ "preserve": JsxEmit.Preserve, "react-native": JsxEmit.ReactNative, - "react": JsxEmit.React + "react": JsxEmit.React, + "vue": JsxEmit.Vue }), paramType: Diagnostics.KIND, showInSimplifiedHelpView: true, diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index b8b77193ed067..331658ab5b32c 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -3205,16 +3205,16 @@ namespace ts { ); } - function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { + function createSynthesizedIdentifier(name: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { // 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. - const react = createIdentifier(reactNamespace || "React"); - react.flags &= ~NodeFlags.Synthesized; + const ident = createIdentifier(name); + ident.flags &= ~NodeFlags.Synthesized; // 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 = getParseTreeNode(parent); - return react; + ident.parent = getParseTreeNode(parent); + return ident; } function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { @@ -3225,44 +3225,55 @@ namespace ts { return createPropertyAccess(left, right); } else { - return createReactNamespace(idText(jsxFactory), parent); + return createSynthesizedIdentifier(idText(jsxFactory), parent); } } - function createJsxFactoryExpression(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { + function createJsxFactoryExpression(jsxFactoryEntity: EntityName | undefined, jsxMode: JsxEmit, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { return jsxFactoryEntity ? createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) : + createJsxCreateElement(jsxMode, reactNamespace, parent); + } + + function createJsxCreateElement(jsxMode: JsxEmit, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { + return jsxMode === JsxEmit.Vue ? + createSynthesizedIdentifier("h", parent) : createPropertyAccess( - createReactNamespace(reactNamespace, parent), + createSynthesizedIdentifier(reactNamespace || "React", parent), "createElement" ); } - export function createExpressionForJsxElement(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, tagName: Expression, props: Expression, children: ReadonlyArray, parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression { + export function createExpressionForJsxElement(jsxFactoryEntity: EntityName | undefined, jsxMode: JsxEmit, reactNamespace: string, tagName: Expression, props: Expression, children: ReadonlyArray, parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression { const argumentsList = [tagName]; if (props) { argumentsList.push(props); } if (children && children.length > 0) { - if (!props) { - argumentsList.push(createNull()); + if (jsxMode === JsxEmit.Vue) { + argumentsList.push(createArrayLiteral(children)); } + else { + if (!props) { + argumentsList.push(createNull()); + } - if (children.length > 1) { - for (const child of children) { - startOnNewLine(child); - argumentsList.push(child); + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); + } + } + else { + argumentsList.push(children[0]); } - } - else { - argumentsList.push(children[0]); } } return setTextRange( createCall( - createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), + createJsxFactoryExpression(jsxFactoryEntity, jsxMode, reactNamespace, parentElement), /*typeArguments*/ undefined, argumentsList ), @@ -3270,30 +3281,35 @@ namespace ts { ); } - export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, children: ReadonlyArray, parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { + export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, jsxMode: JsxEmit, reactNamespace: string, children: ReadonlyArray, parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { const tagName = createPropertyAccess( - createReactNamespace(reactNamespace, parentElement), + createSynthesizedIdentifier(reactNamespace || "React", parentElement), "Fragment" ); const argumentsList = [tagName]; - argumentsList.push(createNull()); + if (jsxMode === JsxEmit.Vue) { + argumentsList.push(createArrayLiteral(children)); + } + else { + argumentsList.push(createNull()); - if (children && children.length > 0) { - if (children.length > 1) { - for (const child of children) { - startOnNewLine(child); - argumentsList.push(child); + if (children && children.length > 0) { + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); + } + } + else { + argumentsList.push(children[0]); } - } - else { - argumentsList.push(children[0]); } } return setTextRange( createCall( - createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), + createJsxFactoryExpression(jsxFactoryEntity, jsxMode, reactNamespace, parentElement), /*typeArguments*/ undefined, argumentsList ), diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index b78202e0c3d58..4da8a975757c5 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -34,7 +34,7 @@ namespace ts { transformers.push(transformTypeScript); - if (jsx === JsxEmit.React) { + if (jsx === JsxEmit.React || jsx === JsxEmit.Vue) { transformers.push(transformJsx); } diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index c31cdd17f027c..7055c539873d8 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -88,36 +88,20 @@ namespace ts { const tagName = getTagName(node); let objectProperties: Expression | undefined; const attrs = node.attributes.properties; - if (attrs.length === 0) { + if (compilerOptions.jsx === JsxEmit.Vue) { + objectProperties = createVueProperties(tagName, attrs); + } + else if (attrs.length === 0) { // When there are no attributes, React wants "null" objectProperties = createNull(); } else { - // Map spans of JsxAttribute nodes into object literals and spans - // of JsxSpreadAttribute nodes into expressions. - const segments = flatten( - spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread - ? map(attrs, transformJsxSpreadAttributeToExpression) - : createObjectLiteral(map(attrs, transformJsxAttributeToObjectLiteralElement)) - ) - ); - - if (isJsxSpreadAttribute(attrs[0])) { - // We must always emit at least one object literal before a spread - // argument. - segments.unshift(createObjectLiteral()); - } - - // Either emit one big object literal (no spread attribs), or - // a call to the __assign helper. - objectProperties = singleOrUndefined(segments); - if (!objectProperties) { - objectProperties = createAssignHelper(context, segments); - } + objectProperties = createReactProperties(attrs); } const element = createExpressionForJsxElement( context.getEmitResolver().getJsxFactoryEntity(currentSourceFile), + compilerOptions.jsx!, compilerOptions.reactNamespace!, // TODO: GH#18217 tagName, objectProperties, @@ -133,9 +117,106 @@ namespace ts { return element; } + function createVueProperties(tagName: Expression, attrs: NodeArray) { + // it is a custom element if it contains a hypen or it is not a string literal + // Vue treats DOM elements and custom elements differently + let isCustomElement = true; + if (tagName.kind === SyntaxKind.StringLiteral && (tagName).text.indexOf("-") === -1) { + isCustomElement = false; + } + + return createObjectLiteral(spanMap( + attrs, + attr => getVuePropertyKind(isCustomElement, attr), + createVuePropertyAssignment + )); + } + + function getVuePropertyKind(isCustomElement: boolean, attr: JsxAttributeLike) { + if (isJsxSpreadAttribute(attr)) { + /* + const expression = transformJsxSpreadAttributeToExpression(attr); + const type = expression.contextualType; + if (type && type.symbol.members && type.symbol.members.size > 0) + console.log(type.symbol.members); + */ + throw new Error("Not implemented"); + } + + const name = idText(attr.name); + + if (["class", "style", "slot", "key", "ref"].indexOf(name) > -1) { + return name; + } + else if (name.indexOf("v-") === 0) { + return "directives"; + } + else if (name.indexOf("on") === 0) { + return "on"; + } + else { + return isCustomElement + ? "props" + : (["value", "innerHTML"].indexOf(name) > -1 ? "domProps" : "attrs"); + } + } + + function createVuePropertyAssignment(attrs: JsxAttribute[], key: string) { + if (["class", "style", "slot", "key", "ref"].indexOf(key) > -1) { + return transformJsxAttributeToObjectLiteralElement(attrs[0]); + } + else if (key === "directives") { + return createPropertyAssignment(key, createArrayLiteral(map(attrs, createVueDirectiveObjectLiteral))); + } + else { + return createPropertyAssignment(key, createObjectLiteral(map(attrs, transformVueAttributeToObjectLiteralElement))); + } + } + + function createVueDirectiveObjectLiteral(attr: JsxAttribute) { + const name = idText(attr.name).substr(2); + return createObjectLiteral([ + createPropertyAssignment("name", createStringLiteral(name)), + createPropertyAssignment("value", transformJsxAttributeInitializer(attr.initializer)) + ]); + } + + function transformVueAttributeToObjectLiteralElement(attr: JsxAttribute) { + let name = idText(attr.name); + if (name.indexOf("on") === 0) { + name = name.substr(2); + } + const propName = name.indexOf("-") > -1 ? createStringLiteral(name) : name; + const expression = transformJsxAttributeInitializer(attr.initializer); + return createPropertyAssignment(propName, expression); + } + + function createReactProperties(attrs: NodeArray) { + let objectProperties: Expression | undefined; + // Map spans of JsxAttribute nodes into object literals and spans + // of JsxSpreadAttribute nodes into expressions. + const segments = flatten(spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread + ? map(attrs, transformJsxSpreadAttributeToExpression) + : createObjectLiteral(map(attrs, transformJsxAttributeToObjectLiteralElement)))); + if (isJsxSpreadAttribute(attrs[0])) { + // We must always emit at least one object literal before a spread + // argument. + segments.unshift(createObjectLiteral()); + } + // Either emit one big object literal (no spread attribs), or + // a call to the __assign helper. + objectProperties = singleOrUndefined(segments); + if (!objectProperties) { + objectProperties = createAssignHelper(context, segments); + } + return objectProperties; + } + + function visitJsxOpeningFragment(node: JsxOpeningFragment, children: ReadonlyArray, isChild: boolean, location: TextRange) { const element = createExpressionForJsxFragment( context.getEmitResolver().getJsxFactoryEntity(currentSourceFile), + compilerOptions.jsx!, compilerOptions.reactNamespace!, // TODO: GH#18217 mapDefined(children, transformJsxChildToExpression), node, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 64ee4a28f2c34..d4897298cf465 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4413,7 +4413,8 @@ namespace ts { None = 0, Preserve = 1, React = 2, - ReactNative = 3 + ReactNative = 3, + Vue = 4 } export const enum NewLineKind {