mirror of
				https://github.com/vuejs/babel-plugin-jsx.git
				synced 2025-11-04 11:22:19 +08:00 
			
		
		
		
	support mergeProps
This commit is contained in:
		
							
								
								
									
										4
									
								
								.babelrc
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.babelrc
									
									
									
									
									
								
							@@ -1,8 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "presets": [
 | 
					  "presets": [
 | 
				
			||||||
    "@babel/env"
 | 
					    "@babel/env",
 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  "plugins": [
 | 
					 | 
				
			||||||
    "./src/index.js"
 | 
					    "./src/index.js"
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.jest.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								.jest.js
									
									
									
									
									
								
							@@ -1,5 +1,7 @@
 | 
				
			|||||||
 | 
					const { h, mergeProps } = require('vue');
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  globals: {
 | 
					  globals: {
 | 
				
			||||||
    "_h": require('vue').h // TODO: for jest error  _h is not defined
 | 
					    "_h": h,
 | 
				
			||||||
 | 
					    "_mergeProps": mergeProps
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										287
									
								
								src/babel-plugin-transform-vue-jsx.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/babel-plugin-transform-vue-jsx.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,287 @@
 | 
				
			|||||||
 | 
					const syntaxJsx = require('@babel/plugin-syntax-jsx').default;
 | 
				
			||||||
 | 
					const t = require('@babel/types');
 | 
				
			||||||
 | 
					const htmlTags = require('html-tags');
 | 
				
			||||||
 | 
					const svgTags = require('svg-tags');
 | 
				
			||||||
 | 
					const helperModuleImports = require('@babel/helper-module-imports');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const xlinkRE = /^xlink([A-Z])/;
 | 
				
			||||||
 | 
					const eventRE = /^on[A-Z][a-z]+$/;
 | 
				
			||||||
 | 
					const rootAttributes = ['class', 'style'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * click --> onClick
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformOn = (event = '') => `on${event[0].toUpperCase()}${event.substr(1)}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filterEmpty = (value) => value !== undefined && value !== null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transform JSXMemberExpression to MemberExpression
 | 
				
			||||||
 | 
					 * @param path JSXMemberExpression
 | 
				
			||||||
 | 
					 * @returns MemberExpression
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const transformJSXMemberExpression = (path) => {
 | 
				
			||||||
 | 
					  const objectPath = path.get('object');
 | 
				
			||||||
 | 
					  const propertyPath = path.get('property');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const transformedObject = objectPath.isJSXMemberExpression()
 | 
				
			||||||
 | 
					    ? transformJSXMemberExpression(objectPath)
 | 
				
			||||||
 | 
					    : objectPath.isJSXIdentifier()
 | 
				
			||||||
 | 
					      ? t.identifier(objectPath.node.name)
 | 
				
			||||||
 | 
					      : t.nullLiteral();
 | 
				
			||||||
 | 
					  const transformedProperty = t.identifier(propertyPath.get('name').node);
 | 
				
			||||||
 | 
					  return t.memberExpression(transformedObject, transformedProperty);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get tag (first attribute for h) from JSXOpeningElement
 | 
				
			||||||
 | 
					 * @param path JSXOpeningElement
 | 
				
			||||||
 | 
					 * @returns Identifier | StringLiteral | MemberExpression
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const getTag = (path) => {
 | 
				
			||||||
 | 
					  const namePath = path.get('openingElement').get('name');
 | 
				
			||||||
 | 
					  if (namePath.isJSXIdentifier()) {
 | 
				
			||||||
 | 
					    const { name } = namePath.node;
 | 
				
			||||||
 | 
					    if (path.scope.hasBinding(name) && !htmlTags.includes(name) && !svgTags.includes(name)) {
 | 
				
			||||||
 | 
					      return t.identifier(name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return t.stringLiteral(name);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (namePath.isJSXMemberExpression()) {
 | 
				
			||||||
 | 
					    return transformJSXMemberExpression(namePath);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  throw new Error(`getTag: ${namePath.type} is not supported`);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getJSXAttributeName = (path) => {
 | 
				
			||||||
 | 
					  const nameNode = path.node.name;
 | 
				
			||||||
 | 
					  if (t.isJSXIdentifier(nameNode)) {
 | 
				
			||||||
 | 
					    return nameNode.name;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return `${nameNode.namespace.name}:${nameNode.name.name}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getJSXAttributeValue = (path, injected) => {
 | 
				
			||||||
 | 
					  const valuePath = path.get('value');
 | 
				
			||||||
 | 
					  if (valuePath.isJSXElement()) {
 | 
				
			||||||
 | 
					    return transformJSXElement(valuePath, injected);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (valuePath.isStringLiteral()) {
 | 
				
			||||||
 | 
					    return valuePath.node;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (valuePath.isJSXExpressionContainer()) {
 | 
				
			||||||
 | 
					    return transformJSXExpressionContainer(valuePath);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformJSXAttribute = (path, attributesToMerge, injected) => {
 | 
				
			||||||
 | 
					  let name = getJSXAttributeName(path);
 | 
				
			||||||
 | 
					  if (name === 'on') {
 | 
				
			||||||
 | 
					    const { properties = [] } = getJSXAttributeValue(path);
 | 
				
			||||||
 | 
					    properties.forEach((property) => {
 | 
				
			||||||
 | 
					      property.key = t.identifier(transformOn(property.key.name));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return t.spreadElement(t.objectExpression(properties));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (rootAttributes.includes(name) || eventRE.test(name)) {
 | 
				
			||||||
 | 
					    attributesToMerge.push(
 | 
				
			||||||
 | 
					      t.objectExpression([
 | 
				
			||||||
 | 
					        t.objectProperty(
 | 
				
			||||||
 | 
					          t.stringLiteral(
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          getJSXAttributeValue(path, injected),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ]),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (name.match(xlinkRE)) {
 | 
				
			||||||
 | 
					    name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return t.objectProperty(
 | 
				
			||||||
 | 
					    t.stringLiteral(
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    getJSXAttributeValue(path, injected) || t.booleanLiteral(true),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformJSXSpreadAttribute = (path, attributesToMerge) => {
 | 
				
			||||||
 | 
					  const argument = path.get('argument').node;
 | 
				
			||||||
 | 
					  const { properties } = argument;
 | 
				
			||||||
 | 
					  if (!properties) {
 | 
				
			||||||
 | 
					    return t.spreadElement(argument);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return t.spreadElement(t.objectExpression(properties.filter((property) => {
 | 
				
			||||||
 | 
					    const { key, value } = property;
 | 
				
			||||||
 | 
					    const name = key.value;
 | 
				
			||||||
 | 
					    if (rootAttributes.includes(name)) {
 | 
				
			||||||
 | 
					      attributesToMerge.push(
 | 
				
			||||||
 | 
					        t.objectExpression([
 | 
				
			||||||
 | 
					          t.objectProperty(
 | 
				
			||||||
 | 
					            t.stringLiteral(name),
 | 
				
			||||||
 | 
					            value,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ]),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  })));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformAttribute = (path, attributesToMerge, injected) => (path.isJSXAttribute()
 | 
				
			||||||
 | 
					  ? transformJSXAttribute(path, attributesToMerge, injected)
 | 
				
			||||||
 | 
					  : transformJSXSpreadAttribute(path, attributesToMerge));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getAttributes = (path, injected) => {
 | 
				
			||||||
 | 
					  const attributes = path.get('openingElement').get('attributes');
 | 
				
			||||||
 | 
					  if (attributes.length === 0) {
 | 
				
			||||||
 | 
					    return t.nullLiteral();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const attributesToMerge = [];
 | 
				
			||||||
 | 
					  const attributeArray = [];
 | 
				
			||||||
 | 
					  attributes
 | 
				
			||||||
 | 
					    .forEach((attribute) => {
 | 
				
			||||||
 | 
					      const attr = transformAttribute(attribute, attributesToMerge, injected);
 | 
				
			||||||
 | 
					      if (attr) {
 | 
				
			||||||
 | 
					        attributeArray.push(attr);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  return t.callExpression(
 | 
				
			||||||
 | 
					    injected.mergeProps,
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      ...attributesToMerge,
 | 
				
			||||||
 | 
					      t.objectExpression(attributeArray),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transform JSXText to StringLiteral
 | 
				
			||||||
 | 
					 * @param path JSXText
 | 
				
			||||||
 | 
					 * @returns StringLiteral
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const transformJSXText = (path) => {
 | 
				
			||||||
 | 
					  const { node } = path;
 | 
				
			||||||
 | 
					  const lines = node.value.split(/\r\n|\n|\r/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let lastNonEmptyLine = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (let i = 0; i < lines.length; i++) {
 | 
				
			||||||
 | 
					    if (lines[i].match(/[^ \t]/)) {
 | 
				
			||||||
 | 
					      lastNonEmptyLine = i;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let str = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (let i = 0; i < lines.length; i++) {
 | 
				
			||||||
 | 
					    const line = lines[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isFirstLine = i === 0;
 | 
				
			||||||
 | 
					    const isLastLine = i === lines.length - 1;
 | 
				
			||||||
 | 
					    const isLastNonEmptyLine = i === lastNonEmptyLine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // replace rendered whitespace tabs with spaces
 | 
				
			||||||
 | 
					    let trimmedLine = line.replace(/\t/g, ' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // trim whitespace touching a newline
 | 
				
			||||||
 | 
					    if (!isFirstLine) {
 | 
				
			||||||
 | 
					      trimmedLine = trimmedLine.replace(/^[ ]+/, '');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // trim whitespace touching an endline
 | 
				
			||||||
 | 
					    if (!isLastLine) {
 | 
				
			||||||
 | 
					      trimmedLine = trimmedLine.replace(/[ ]+$/, '');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (trimmedLine) {
 | 
				
			||||||
 | 
					      if (!isLastNonEmptyLine) {
 | 
				
			||||||
 | 
					        trimmedLine += ' ';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      str += trimmedLine;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return str !== '' ? t.stringLiteral(str) : null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transform JSXExpressionContainer to Expression
 | 
				
			||||||
 | 
					 * @param path JSXExpressionContainer
 | 
				
			||||||
 | 
					 * @returns Expression
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const transformJSXExpressionContainer = (path) => path.get('expression').node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Transform JSXSpreadChild
 | 
				
			||||||
 | 
					 * @param path JSXSpreadChild
 | 
				
			||||||
 | 
					 * @returns SpreadElement
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const transformJSXSpreadChild = (path) => t.spreadElement(path.get('expression').node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get children from Array of JSX children
 | 
				
			||||||
 | 
					 * @param paths Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement>
 | 
				
			||||||
 | 
					 * @param injected {}
 | 
				
			||||||
 | 
					 * @returns Array<Expression | SpreadElement>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const getChildren = (paths, injected) => paths
 | 
				
			||||||
 | 
					  .map((path) => {
 | 
				
			||||||
 | 
					    if (path.isJSXText()) {
 | 
				
			||||||
 | 
					      return transformJSXText(path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (path.isJSXExpressionContainer()) {
 | 
				
			||||||
 | 
					      return transformJSXExpressionContainer(path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (path.isJSXSpreadChild()) {
 | 
				
			||||||
 | 
					      return transformJSXSpreadChild(path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (path.isCallExpression()) {
 | 
				
			||||||
 | 
					      return path.node;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (path.isJSXElement()) {
 | 
				
			||||||
 | 
					      return transformJSXElement(path, injected);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw new Error(`getChildren: ${path.type} is not supported`);
 | 
				
			||||||
 | 
					  }).filter(filterEmpty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const transformJSXElement = (path, injected) => t.callExpression(injected.h, [
 | 
				
			||||||
 | 
					  getTag(path),
 | 
				
			||||||
 | 
					  getAttributes(path, injected),
 | 
				
			||||||
 | 
					  t.arrayExpression(getChildren(path.get('children'), injected)),
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = () => ({
 | 
				
			||||||
 | 
					  name: 'babel-plugin-transform-vue-jsx',
 | 
				
			||||||
 | 
					  inherits: syntaxJsx,
 | 
				
			||||||
 | 
					  visitor: {
 | 
				
			||||||
 | 
					    JSXElement: {
 | 
				
			||||||
 | 
					      exit(path, state) {
 | 
				
			||||||
 | 
					        if (!state.vueCreateElementInjected) {
 | 
				
			||||||
 | 
					          state.vueCreateElementInjected = helperModuleImports.addNamed(path, 'h', 'vue');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!state.vueMergePropsInjected) {
 | 
				
			||||||
 | 
					          state.vueMergePropsInjected = helperModuleImports.addNamed(path, 'mergeProps', 'vue');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        path.replaceWith(
 | 
				
			||||||
 | 
					          transformJSXElement(path, {
 | 
				
			||||||
 | 
					            h: state.vueCreateElementInjected,
 | 
				
			||||||
 | 
					            mergeProps: state.vueMergePropsInjected,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										236
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										236
									
								
								src/index.js
									
									
									
									
									
								
							@@ -1,235 +1,7 @@
 | 
				
			|||||||
const syntaxJsx = require('@babel/plugin-syntax-jsx').default;
 | 
					const babelPluginTransformVueJsx = require('./babel-plugin-transform-vue-jsx');
 | 
				
			||||||
const t = require('@babel/types');
 | 
					 | 
				
			||||||
const htmlTags = require('html-tags');
 | 
					 | 
				
			||||||
const svgTags = require('svg-tags');
 | 
					 | 
				
			||||||
const helperModuleImports = require('@babel/helper-module-imports');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const xlinkRE = /^xlink([A-Z])/;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * click --> onClick
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const transformOn = (event = '') => `on${event[0].toUpperCase()}${event.substr(1)}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const filterEmpty = (value) => value !== undefined && value !== null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transform JSXMemberExpression to MemberExpression
 | 
					 | 
				
			||||||
 * @param path JSXMemberExpression
 | 
					 | 
				
			||||||
 * @returns MemberExpression
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const transformJSXMemberExpression = (path) => {
 | 
					 | 
				
			||||||
  const objectPath = path.get('object');
 | 
					 | 
				
			||||||
  const propertyPath = path.get('property');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const transformedObject = objectPath.isJSXMemberExpression()
 | 
					 | 
				
			||||||
    ? transformJSXMemberExpression(objectPath)
 | 
					 | 
				
			||||||
    : objectPath.isJSXIdentifier()
 | 
					 | 
				
			||||||
      ? t.identifier(objectPath.node.name)
 | 
					 | 
				
			||||||
      : t.nullLiteral();
 | 
					 | 
				
			||||||
  const transformedProperty = t.identifier(propertyPath.get('name').node);
 | 
					 | 
				
			||||||
  return t.memberExpression(transformedObject, transformedProperty);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Get tag (first attribute for h) from JSXOpeningElement
 | 
					 | 
				
			||||||
 * @param path JSXOpeningElement
 | 
					 | 
				
			||||||
 * @returns Identifier | StringLiteral | MemberExpression
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const getTag = (path) => {
 | 
					 | 
				
			||||||
  const namePath = path.get('openingElement').get('name');
 | 
					 | 
				
			||||||
  if (namePath.isJSXIdentifier()) {
 | 
					 | 
				
			||||||
    const { name } = namePath.node;
 | 
					 | 
				
			||||||
    if (path.scope.hasBinding(name) && !htmlTags.includes(name) && !svgTags.includes(name)) {
 | 
					 | 
				
			||||||
      return t.identifier(name);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return t.stringLiteral(name);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (namePath.isJSXMemberExpression()) {
 | 
					 | 
				
			||||||
    return transformJSXMemberExpression(namePath);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  throw new Error(`getTag: ${namePath.type} is not supported`);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getJSXAttributeName = (path) => {
 | 
					 | 
				
			||||||
  const nameNode = path.node.name;
 | 
					 | 
				
			||||||
  if (t.isJSXIdentifier(nameNode)) {
 | 
					 | 
				
			||||||
    return nameNode.name;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return `${nameNode.namespace.name}:${nameNode.name.name}`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getJSXAttributeValue = (path, injected) => {
 | 
					 | 
				
			||||||
  const valuePath = path.get('value');
 | 
					 | 
				
			||||||
  if (valuePath.isJSXElement()) {
 | 
					 | 
				
			||||||
    return transformJSXElement(valuePath, injected);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (valuePath.isStringLiteral()) {
 | 
					 | 
				
			||||||
    return valuePath.node;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (valuePath.isJSXExpressionContainer()) {
 | 
					 | 
				
			||||||
    return transformJSXExpressionContainer(valuePath);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return null;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const transformJSXAttribute = (path, injected) => {
 | 
					 | 
				
			||||||
  let name = getJSXAttributeName(path);
 | 
					 | 
				
			||||||
  if (name === 'on') {
 | 
					 | 
				
			||||||
    const { properties = [] } = getJSXAttributeValue(path);
 | 
					 | 
				
			||||||
    properties.forEach((property) => {
 | 
					 | 
				
			||||||
      property.key = t.identifier(transformOn(property.key.name));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    return t.spreadElement(t.objectExpression(properties));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  if (name.match(xlinkRE)) {
 | 
					 | 
				
			||||||
    name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return t.objectProperty(
 | 
					 | 
				
			||||||
    t.stringLiteral(
 | 
					 | 
				
			||||||
      name,
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    getJSXAttributeValue(path, injected) || t.booleanLiteral(true),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const transformJSXSpreadAttribute = (path) => t.spreadElement(path.get('argument').node);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const transformAttribute = (path, injected) => (path.isJSXAttribute()
 | 
					 | 
				
			||||||
  ? transformJSXAttribute(path, injected)
 | 
					 | 
				
			||||||
  : transformJSXSpreadAttribute(path));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getAttributes = (path, injected) => {
 | 
					 | 
				
			||||||
  const attributes = path.get('openingElement').get('attributes');
 | 
					 | 
				
			||||||
  if (attributes.length === 0) {
 | 
					 | 
				
			||||||
    return t.nullLiteral();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  // return t.callExpression(injected.mergeProps, [attributes
 | 
					 | 
				
			||||||
  //   .map((el) => transformAttribute(el, injected))]);
 | 
					 | 
				
			||||||
  return t.objectExpression(attributes
 | 
					 | 
				
			||||||
    .map((el) => transformAttribute(el, injected)));
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transform JSXText to StringLiteral
 | 
					 | 
				
			||||||
 * @param path JSXText
 | 
					 | 
				
			||||||
 * @returns StringLiteral
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const transformJSXText = (path) => {
 | 
					 | 
				
			||||||
  const { node } = path;
 | 
					 | 
				
			||||||
  const lines = node.value.split(/\r\n|\n|\r/);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let lastNonEmptyLine = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (let i = 0; i < lines.length; i++) {
 | 
					 | 
				
			||||||
    if (lines[i].match(/[^ \t]/)) {
 | 
					 | 
				
			||||||
      lastNonEmptyLine = i;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let str = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (let i = 0; i < lines.length; i++) {
 | 
					 | 
				
			||||||
    const line = lines[i];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isFirstLine = i === 0;
 | 
					 | 
				
			||||||
    const isLastLine = i === lines.length - 1;
 | 
					 | 
				
			||||||
    const isLastNonEmptyLine = i === lastNonEmptyLine;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // replace rendered whitespace tabs with spaces
 | 
					 | 
				
			||||||
    let trimmedLine = line.replace(/\t/g, ' ');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // trim whitespace touching a newline
 | 
					 | 
				
			||||||
    if (!isFirstLine) {
 | 
					 | 
				
			||||||
      trimmedLine = trimmedLine.replace(/^[ ]+/, '');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // trim whitespace touching an endline
 | 
					 | 
				
			||||||
    if (!isLastLine) {
 | 
					 | 
				
			||||||
      trimmedLine = trimmedLine.replace(/[ ]+$/, '');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (trimmedLine) {
 | 
					 | 
				
			||||||
      if (!isLastNonEmptyLine) {
 | 
					 | 
				
			||||||
        trimmedLine += ' ';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      str += trimmedLine;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return str !== '' ? t.stringLiteral(str) : null;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transform JSXExpressionContainer to Expression
 | 
					 | 
				
			||||||
 * @param path JSXExpressionContainer
 | 
					 | 
				
			||||||
 * @returns Expression
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const transformJSXExpressionContainer = (path) => path.get('expression').node;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transform JSXSpreadChild
 | 
					 | 
				
			||||||
 * @param path JSXSpreadChild
 | 
					 | 
				
			||||||
 * @returns SpreadElement
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const transformJSXSpreadChild = (path) => t.spreadElement(path.get('expression').node);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Get children from Array of JSX children
 | 
					 | 
				
			||||||
 * @param paths Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement>
 | 
					 | 
				
			||||||
 * @param injected {}
 | 
					 | 
				
			||||||
 * @returns Array<Expression | SpreadElement>
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const getChildren = (paths, injected) => paths
 | 
					 | 
				
			||||||
  .map((path) => {
 | 
					 | 
				
			||||||
    if (path.isJSXText()) {
 | 
					 | 
				
			||||||
      return transformJSXText(path);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (path.isJSXExpressionContainer()) {
 | 
					 | 
				
			||||||
      return transformJSXExpressionContainer(path);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (path.isJSXSpreadChild()) {
 | 
					 | 
				
			||||||
      return transformJSXSpreadChild(path);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (path.isCallExpression()) {
 | 
					 | 
				
			||||||
      return path.node;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (path.isJSXElement()) {
 | 
					 | 
				
			||||||
      return transformJSXElement(path, injected);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    throw new Error(`getChildren: ${path.type} is not supported`);
 | 
					 | 
				
			||||||
  }).filter(filterEmpty);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const transformJSXElement = (path, injected) => t.callExpression(injected.h, [
 | 
					 | 
				
			||||||
  getTag(path),
 | 
					 | 
				
			||||||
  getAttributes(path, injected),
 | 
					 | 
				
			||||||
  t.arrayExpression(getChildren(path.get('children'), injected)),
 | 
					 | 
				
			||||||
]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = () => ({
 | 
					module.exports = () => ({
 | 
				
			||||||
  inherits: syntaxJsx,
 | 
					  plugins: [
 | 
				
			||||||
  visitor: {
 | 
					    babelPluginTransformVueJsx,
 | 
				
			||||||
    JSXElement: {
 | 
					  ],
 | 
				
			||||||
      exit(path, state) {
 | 
					 | 
				
			||||||
        if (!state.vueCreateElementInjected) {
 | 
					 | 
				
			||||||
          state.vueCreateElementInjected = helperModuleImports.addNamed(path, 'h', 'vue');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!state.vueMergePropsInjected) {
 | 
					 | 
				
			||||||
          state.vueMergePropsInjected = helperModuleImports.addNamed(path, 'mergeProps', 'vue');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        path.replaceWith(
 | 
					 | 
				
			||||||
          transformJSXElement(path, {
 | 
					 | 
				
			||||||
            h: state.vueCreateElementInjected,
 | 
					 | 
				
			||||||
            mergeProps: state.vueMergePropsInjected,
 | 
					 | 
				
			||||||
          }),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,10 +64,40 @@ test('xlink:href', () => {
 | 
				
			|||||||
  expect(wrapper.attributes()['xlink:href']).toBe('#name');
 | 
					  expect(wrapper.attributes()['xlink:href']).toBe('#name');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// // test('Merge class', () => {
 | 
					test('Merge class', () => {
 | 
				
			||||||
// //   const wrapper = render(() => <div class="a" {...{ class: 'b' } } />);
 | 
					  const wrapper = shallowMount({
 | 
				
			||||||
// //   expect(wrapper.innerHTML).toBe('<div class="a b"></div>');
 | 
					    setup() {
 | 
				
			||||||
// // });
 | 
					      return () => <div class="a" {...{ class: 'b' } } />;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  expect(wrapper.html()).toBe('<div class="a b"></div>');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Merge style', () => {
 | 
				
			||||||
 | 
					  const propsA = {
 | 
				
			||||||
 | 
					    style: {
 | 
				
			||||||
 | 
					      color: 'red',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const propsB = {
 | 
				
			||||||
 | 
					    style: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        color: 'blue',
 | 
				
			||||||
 | 
					        width: '200px',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        width: '300px',
 | 
				
			||||||
 | 
					        height: '300px',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const wrapper = shallowMount({
 | 
				
			||||||
 | 
					    setup() {
 | 
				
			||||||
 | 
					      return () => <div { ...propsA } { ...propsB } />;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  expect(wrapper.html()).toBe('<div style="color: blue; width: 300px; height: 300px;"></div>');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('JSXSpreadChild', () => {
 | 
					test('JSXSpreadChild', () => {
 | 
				
			||||||
  const a = ['1', '2'];
 | 
					  const a = ['1', '2'];
 | 
				
			||||||
@@ -76,7 +106,7 @@ test('JSXSpreadChild', () => {
 | 
				
			|||||||
      return () => <div>{[...a]}</div>;
 | 
					      return () => <div>{[...a]}</div>;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  expect(wrapper.text).toBe('12');
 | 
					  expect(wrapper.text()).toBe('12');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('domProps input[value]', () => {
 | 
					test('domProps input[value]', () => {
 | 
				
			||||||
@@ -88,3 +118,42 @@ test('domProps input[value]', () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
  expect(wrapper.html()).toBe('<input type="text">');
 | 
					  expect(wrapper.html()).toBe('<input type="text">');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Spread (single object expression)', () => {
 | 
				
			||||||
 | 
					  const props = {
 | 
				
			||||||
 | 
					    innerHTML: 123,
 | 
				
			||||||
 | 
					    other: '1',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const wrapper = shallowMount({
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					      return <div {...props}></div>;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  expect(wrapper.html()).toBe('<div other="1">123</div>');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Spread (mixed)', () => {
 | 
				
			||||||
 | 
					  const calls = [];
 | 
				
			||||||
 | 
					  const data = {
 | 
				
			||||||
 | 
					    id: 'hehe',
 | 
				
			||||||
 | 
					    onClick() {
 | 
				
			||||||
 | 
					      calls.push(3);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    innerHTML: 2,
 | 
				
			||||||
 | 
					    class: ['a', 'b'],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  shallowMount({
 | 
				
			||||||
 | 
					    setup() {
 | 
				
			||||||
 | 
					      return () => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          href="huhu"
 | 
				
			||||||
 | 
					          {...data}
 | 
				
			||||||
 | 
					          class={{ c: true }}
 | 
				
			||||||
 | 
					          onClick={() => calls.push(4)}
 | 
				
			||||||
 | 
					          hook-insert={() => calls.push(2)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user