diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..87db373
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": [
+ "@babel/env"
+ ],
+ "plugins": [
+ "./src/index.js"
+ ]
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..0129706
--- /dev/null
+++ b/.eslintrc
@@ -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]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 6704566..cab290e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,5 @@ dist
# TernJS port file
.tern-port
+
+package-lock.json
diff --git a/.jest.js b/.jest.js
new file mode 100644
index 0000000..e71c516
--- /dev/null
+++ b/.jest.js
@@ -0,0 +1,5 @@
+module.exports = {
+ globals: {
+ "_h": require('vue').h // TODO: for jest error _h is not defined
+ }
+}
diff --git a/README.md b/README.md
index 58eef75..070e556 100644
--- a/README.md
+++ b/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 = () =>
+```
+
+with setup render
+
+```jsx
+const App = defineComponent(() => {
+ const count = ref(0);
+ return () => (
+
+ {count.value}
+
+ )
+})
+```
diff --git a/example/index.js b/example/index.js
new file mode 100644
index 0000000..64f73d9
--- /dev/null
+++ b/example/index.js
@@ -0,0 +1,40 @@
+import { createApp, ref, defineComponent } from 'vue';
+
+const SuperButton = (props, context) => (
+
+ Super
+
+
+);
+
+SuperButton.inheritAttrs = false;
+
+const App = defineComponent(() => {
+ const count = ref(0);
+ const inc = () => {
+ count.value++;
+ };
+
+ return () => (
+
+ Foo {count.value}
+
+
+ );
+});
+
+createApp(App).mount('#app');
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a5affc8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..eb447da
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..ede9908
--- /dev/null
+++ b/src/index.js
@@ -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
+ * @param injected {}
+ * @returns Array
+ */
+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,
+ }),
+ );
+ },
+ },
+ },
+});
diff --git a/test/index.test.js b/test/index.test.js
new file mode 100644
index 0000000..dc44570
--- /dev/null
+++ b/test/index.test.js
@@ -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 () => 123
;
+ },
+ };
+
+ const wrapper = render(App);
+ expect(wrapper.innerHTML).toBe('123
');
+});
+
+test('should not fallthrough with inheritAttrs: false', () => {
+ const Child = (props) => {props.foo}
;
+
+ Child.inheritAttrs = false;
+
+ const Parent = () => (
+
+ );
+
+ const wrapper = render(Parent);
+ expect(wrapper.innerHTML).toBe('1
');
+});
+
+
+test('should render', () => {
+ const App = {
+ render() {
+ return 1234
;
+ },
+ };
+ const wrapper = render(App);
+ expect(wrapper.innerHTML).toBe('1234
');
+});
+
+test('xlink:href', () => {
+ const wrapper = render(() => );
+ expect(wrapper.innerHTML).toBe('');
+});
+
+// test('Merge class', () => {
+// const wrapper = render(() => );
+// expect(wrapper.innerHTML).toBe('');
+// });
+
+test('JSXSpreadChild', () => {
+ const a = ['1', '2'];
+ const wrapper = render(() => {[...a]}
);
+ expect(wrapper.innerHTML).toBe('12
');
+});
+
+test('domProps input[value]', () => {
+ const val = 'foo';
+ const wrapper = render(() => );
+ expect(wrapper.innerHTML).toBe('');
+});
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..0a2129b
--- /dev/null
+++ b/webpack.config.js
@@ -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,
+ },
+};