mirror of
				https://github.com/vuejs/babel-plugin-jsx.git
				synced 2025-11-01 01:42:21 +08:00 
			
		
		
		
	jsx
This commit is contained in:
		
							
								
								
									
										8
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "presets": [ | ||||
|     "@babel/env" | ||||
|   ], | ||||
|   "plugins": [ | ||||
|     "./src/index.js" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										26
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| { | ||||
|   "root": true, | ||||
|   "env": { | ||||
|     "browser": true, | ||||
|     "node": true, | ||||
|     "jasmine": true, | ||||
|     "jest": true, | ||||
|     "es6": true | ||||
|   }, | ||||
|   "extends": "eslint-config-airbnb/base", | ||||
|   "parserOptions": { | ||||
|     "parser": "babel-eslint", | ||||
|     "ecmaVersion": 8, | ||||
|     "ecmaFeatures": { | ||||
|       "jsx": true, | ||||
|       "experimentalObjectRestSpread": true | ||||
|     } | ||||
|   }, | ||||
|   "rules": { | ||||
|     "no-nested-ternary": [0], | ||||
|     "no-param-reassign": [0], | ||||
|     "no-use-before-define": [0], | ||||
|     "no-plusplus": [0], | ||||
|     "import/no-extraneous-dependencies": [0] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -102,3 +102,5 @@ dist | ||||
|  | ||||
| # TernJS port file | ||||
| .tern-port | ||||
|  | ||||
| package-lock.json | ||||
|   | ||||
							
								
								
									
										5
									
								
								.jest.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.jest.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| module.exports = { | ||||
|   globals: { | ||||
|     "_h": require('vue').h // TODO: for jest error  _h is not defined | ||||
|   } | ||||
| } | ||||
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,2 +1,24 @@ | ||||
| # jsx | ||||
| jsx for vue 3 | ||||
| # Babel Preset JSX for Vue 3.0 | ||||
|  | ||||
| To add Vue JSX support. | ||||
|  | ||||
| ## Syntax | ||||
|  | ||||
| functional component | ||||
|  | ||||
| ```jsx | ||||
| const App = () => <div></div> | ||||
| ``` | ||||
|  | ||||
| with setup render | ||||
|  | ||||
| ```jsx | ||||
| const App = defineComponent(() => { | ||||
|   const count = ref(0); | ||||
|   return () => ( | ||||
|     <div> | ||||
|       {count.value} | ||||
|     </div> | ||||
|   ) | ||||
| }) | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										40
									
								
								example/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								example/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { createApp, ref, defineComponent } from 'vue'; | ||||
|  | ||||
| const SuperButton = (props, context) => ( | ||||
|     <div class={props.class}> | ||||
|       Super | ||||
|       <button | ||||
|         on={{ | ||||
|           click: () => { | ||||
|             context.emit('click'); | ||||
|           }, | ||||
|         }} | ||||
|       > | ||||
|         { props.buttonText } | ||||
|       </button> | ||||
|     </div> | ||||
| ); | ||||
|  | ||||
| SuperButton.inheritAttrs = false; | ||||
|  | ||||
| const App = defineComponent(() => { | ||||
|   const count = ref(0); | ||||
|   const inc = () => { | ||||
|     count.value++; | ||||
|   }; | ||||
|  | ||||
|   return () => ( | ||||
|     <div> | ||||
|       Foo {count.value} | ||||
|       <SuperButton | ||||
|         buttonText="VueComponent" | ||||
|         class="xxx" | ||||
|         on={{ | ||||
|           click: inc, | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| createApp(App).mount('#app'); | ||||
							
								
								
									
										2
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| <div id="app"></div> | ||||
| <script src="/dist/main.js"></script> | ||||
							
								
								
									
										48
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| { | ||||
|   "name": "jsx", | ||||
|   "version": "1.0.0", | ||||
|   "description": "jsx for vue 3", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "dev": "webpack-dev-server", | ||||
|     "lint": "eslint --ext .js src", | ||||
|     "test": "jest --config .jest.js" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/vueComponent/jsx.git" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "MIT", | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/vueComponent/jsx/issues" | ||||
|   }, | ||||
|   "homepage": "https://github.com/vueComponent/jsx#readme", | ||||
|   "keywords": [ | ||||
|     "vue", | ||||
|     "jsx" | ||||
|   ], | ||||
|   "dependencies": { | ||||
|     "@babel/helper-module-imports": "^7.8.3", | ||||
|     "@babel/plugin-syntax-jsx": "^7.8.3", | ||||
|     "@babel/types": "^7.9.6", | ||||
|     "@vue/babel-preset-jsx": "^1.1.2", | ||||
|     "html-tags": "^3.1.0", | ||||
|     "svg-tags": "^1.0.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.9.6", | ||||
|     "@babel/preset-env": "^7.9.6", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "babel-jest": "^26.0.1", | ||||
|     "babel-loader": "^8.1.0", | ||||
|     "eslint": "^7.0.0", | ||||
|     "eslint-config-airbnb": "^18.1.0", | ||||
|     "eslint-plugin-import": "^2.20.2", | ||||
|     "jest": "^26.0.1", | ||||
|     "vue": "^3.0.0-beta.10", | ||||
|     "webpack": "^4.43.0", | ||||
|     "webpack-cli": "^3.3.11", | ||||
|     "webpack-dev-server": "^3.10.3" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										235
									
								
								src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| 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])/; | ||||
|  | ||||
| /** | ||||
|  * 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 = () => ({ | ||||
|   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, | ||||
|           }), | ||||
|         ); | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										65
									
								
								test/index.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								test/index.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { createApp } from 'vue'; | ||||
|  | ||||
| const render = (app) => { | ||||
|   const root = document.createElement('div'); | ||||
|   document.body.append(root); | ||||
|   createApp(app).mount(root); | ||||
|   return root; | ||||
| }; | ||||
|  | ||||
| test('should render with setup', () => { | ||||
|   const App = { | ||||
|     setup() { | ||||
|       return () => <div>123</div>; | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   const wrapper = render(App); | ||||
|   expect(wrapper.innerHTML).toBe('<div>123</div>'); | ||||
| }); | ||||
|  | ||||
| test('should not fallthrough with inheritAttrs: false', () => { | ||||
|   const Child = (props) => <div>{props.foo}</div>; | ||||
|  | ||||
|   Child.inheritAttrs = false; | ||||
|  | ||||
|   const Parent = () => ( | ||||
|     <Child class="parent" foo={1} /> | ||||
|   ); | ||||
|  | ||||
|   const wrapper = render(Parent); | ||||
|   expect(wrapper.innerHTML).toBe('<div>1</div>'); | ||||
| }); | ||||
|  | ||||
|  | ||||
| test('should render', () => { | ||||
|   const App = { | ||||
|     render() { | ||||
|       return <div>1234</div>; | ||||
|     }, | ||||
|   }; | ||||
|   const wrapper = render(App); | ||||
|   expect(wrapper.innerHTML).toBe('<div>1234</div>'); | ||||
| }); | ||||
|  | ||||
| test('xlink:href', () => { | ||||
|   const wrapper = render(() => <use xlinkHref={'#name'}></use>); | ||||
|   expect(wrapper.innerHTML).toBe('<use xlink:href="#name"></use>'); | ||||
| }); | ||||
|  | ||||
| // test('Merge class', () => { | ||||
| //   const wrapper = render(() => <div class="a" {...{ class: 'b' } } />); | ||||
| //   expect(wrapper.innerHTML).toBe('<div class="a b"></div>'); | ||||
| // }); | ||||
|  | ||||
| test('JSXSpreadChild', () => { | ||||
|   const a = ['1', '2']; | ||||
|   const wrapper = render(() => <div>{[...a]}</div>); | ||||
|   expect(wrapper.innerHTML).toBe('<div>12</div>'); | ||||
| }); | ||||
|  | ||||
| test('domProps input[value]', () => { | ||||
|   const val = 'foo'; | ||||
|   const wrapper = render(() => <input type="text" value={val} />); | ||||
|   expect(wrapper.innerHTML).toBe('<input type="text">'); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| const path = require('path'); | ||||
|  | ||||
| module.exports = { | ||||
|   mode: 'development', | ||||
|   devtool: 'cheap-module-eval-source-map', | ||||
|   entry: './example/index.js', | ||||
|   output: { | ||||
|     path: path.resolve(__dirname, './dist'), | ||||
|     publicPath: '/dist/', | ||||
|   }, | ||||
|   module: { | ||||
|     rules: [ | ||||
|       { | ||||
|         test: /\.jsx?$/, | ||||
|         loader: 'babel-loader', | ||||
|         exclude: /node_modules/, | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   devServer: { | ||||
|     inline: true, | ||||
|     open: true, | ||||
|     hot: true, | ||||
|     overlay: true, | ||||
|   }, | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user