mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-07-02 01:53:28 +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
|
# TernJS port file
|
||||||
.tern-port
|
.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
|
# Babel Preset JSX for Vue 3.0
|
||||||
jsx for vue 3
|
|
||||||
|
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