mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-07-19 22:19:57 +08:00
refactor: support transformOn
This commit is contained in:
13
packages/babel-plugin-jsx/.babelrc
Normal file
13
packages/babel-plugin-jsx/.babelrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
// "modules": "cjs"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
["./src/index.js", { "transformOn": true }]
|
||||
]
|
||||
}
|
10
packages/babel-plugin-jsx/.jest.js
Normal file
10
packages/babel-plugin-jsx/.jest.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { h, mergeProps, withDirectives } = require('vue');
|
||||
|
||||
module.exports = {
|
||||
globals: {
|
||||
'_h': h,
|
||||
'_mergeProps': mergeProps,
|
||||
'_withDirectives': withDirectives
|
||||
},
|
||||
setupFiles: ['./test/setup.js'],
|
||||
}
|
11
packages/babel-plugin-jsx/README.md
Normal file
11
packages/babel-plugin-jsx/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# `babel-plugin-jsx`
|
||||
|
||||
> TODO: description
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
const babelPluginJsx = require('babel-plugin-jsx');
|
||||
|
||||
// TODO: DEMONSTRATE API
|
||||
```
|
53
packages/babel-plugin-jsx/example/index.js
Normal file
53
packages/babel-plugin-jsx/example/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { createApp, ref, defineComponent } from 'vue';
|
||||
|
||||
const SuperButton = (props, context) => {
|
||||
const obj = {
|
||||
mouseover: () => {
|
||||
context.emit('mouseover');
|
||||
},
|
||||
click: () => {
|
||||
context.emit('click');
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div class={props.class}>
|
||||
Super
|
||||
<button
|
||||
on={obj}
|
||||
>
|
||||
{ props.buttonText }
|
||||
{context.slots.default()}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SuperButton.inheritAttrs = false;
|
||||
|
||||
const App = defineComponent(() => {
|
||||
const count = ref(0);
|
||||
const inc = () => {
|
||||
count.value++;
|
||||
};
|
||||
|
||||
const obj = {
|
||||
click: inc,
|
||||
mouseover: inc,
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
Foo {count.value}
|
||||
<SuperButton
|
||||
buttonText="VueComponent"
|
||||
class="xxx"
|
||||
vShow={true}
|
||||
on={obj}
|
||||
>
|
||||
<button>1234</button>
|
||||
</SuperButton>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
createApp(App).mount('#app');
|
2
packages/babel-plugin-jsx/index.html
Normal file
2
packages/babel-plugin-jsx/index.html
Normal file
@ -0,0 +1,2 @@
|
||||
<div id="app"></div>
|
||||
<script src="/dist/main.js"></script>
|
42
packages/babel-plugin-jsx/package.json
Normal file
42
packages/babel-plugin-jsx/package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@ant-design-vue/babel-plugin-jsx",
|
||||
"version": "1.0.0",
|
||||
"description": "Babel plugin for Vue 3.0 JSX",
|
||||
"author": "Amour1688 <lcz_1996@foxmail.com>",
|
||||
"homepage": "https://github.com/vueComponent/jsx/tree/master/packages/babel-plugin-jsx#readme",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vueComponent/jsx.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server",
|
||||
"lint": "eslint --ext .js src",
|
||||
"test": "jest --config .jest.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vueComponent/jsx/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design-vue/babel-helper-vue-transform-on": "^1.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.0.0",
|
||||
"camelcase": "^6.0.0",
|
||||
"html-tags": "^3.1.0",
|
||||
"svg-tags": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@vue/test-utils": "^2.0.0-alpha.6",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"jest": "^26.0.1",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"vue": "^3.0.0-beta.14",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
}
|
||||
}
|
14
packages/babel-plugin-jsx/src/index.js
Normal file
14
packages/babel-plugin-jsx/src/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
const syntaxJsx = require('@babel/plugin-syntax-jsx').default;
|
||||
const tranformVueJSX = require('./transform-vue-jsx');
|
||||
const sugarVModel = require('./sugar-v-model');
|
||||
const sugarFragment = require('./sugar-fragment');
|
||||
|
||||
module.exports = ({ types: t }, opts = {}) => ({
|
||||
name: 'babel-plugin-jsx',
|
||||
inherits: syntaxJsx,
|
||||
visitor: {
|
||||
...sugarVModel(t),
|
||||
...tranformVueJSX(t, opts),
|
||||
...sugarFragment(t),
|
||||
},
|
||||
});
|
22
packages/babel-plugin-jsx/src/sugar-fragment.js
Normal file
22
packages/babel-plugin-jsx/src/sugar-fragment.js
Normal file
@ -0,0 +1,22 @@
|
||||
const helperModuleImports = require('@babel/helper-module-imports');
|
||||
|
||||
const transformFragment = (t, path, { name }) => {
|
||||
const children = path.get('children') || [];
|
||||
return t.jsxElement(
|
||||
t.jsxOpeningElement(t.jsxIdentifier(name), []),
|
||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||
children.map(({ node }) => node),
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = (t) => ({
|
||||
JSXFragment: {
|
||||
enter(path) {
|
||||
if (!path.vueFragment) {
|
||||
path.vueFragment = helperModuleImports.addNamed(path, 'Fragment', 'vue');
|
||||
}
|
||||
path.replaceWith(transformFragment(t, path, path.vueFragment));
|
||||
},
|
||||
},
|
||||
});
|
204
packages/babel-plugin-jsx/src/sugar-v-model.js
Normal file
204
packages/babel-plugin-jsx/src/sugar-v-model.js
Normal file
@ -0,0 +1,204 @@
|
||||
const htmlTags = require('html-tags');
|
||||
const svgTags = require('svg-tags');
|
||||
const camelCase = require('camelcase');
|
||||
const { addNamed } = require('@babel/helper-module-imports');
|
||||
|
||||
|
||||
const cachedCamelCase = (() => {
|
||||
const cache = Object.create(null);
|
||||
return (string) => {
|
||||
if (!cache[string]) {
|
||||
cache[string] = camelCase(string);
|
||||
}
|
||||
|
||||
return cache[string];
|
||||
};
|
||||
})();
|
||||
|
||||
const startsWithCamel = (string, match) => string.startsWith(match)
|
||||
|| string.startsWith(cachedCamelCase(match));
|
||||
|
||||
/**
|
||||
* Add property to a JSX element
|
||||
*
|
||||
* @param t
|
||||
* @param path JSXOpeningElement
|
||||
* @param value string
|
||||
*/
|
||||
const addProp = (path, value) => {
|
||||
path.node.attributes.push(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get JSX element tag name
|
||||
*
|
||||
* @param path Path<JSXOpeningElement>
|
||||
*/
|
||||
const getTagName = (path) => path.get('name.name').node;
|
||||
|
||||
/**
|
||||
* Get JSX element type
|
||||
*
|
||||
* @param t
|
||||
* @param path Path<JSXOpeningElement>
|
||||
*/
|
||||
const getType = (t, path) => {
|
||||
const typePath = path
|
||||
.get('attributes')
|
||||
.find(
|
||||
(attributePath) => t.isJSXAttribute(attributePath)
|
||||
&& t.isJSXIdentifier(attributePath.get('name'))
|
||||
&& attributePath.get('name.name').node === 'type'
|
||||
&& t.isStringLiteral(attributePath.get('value')),
|
||||
);
|
||||
|
||||
return typePath ? typePath.get('value.value').node : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a JSXOpeningElement is a component
|
||||
*
|
||||
* @param t
|
||||
* @param path JSXOpeningElement
|
||||
* @returns boolean
|
||||
*/
|
||||
const isComponent = (t, path) => {
|
||||
const name = path.get('name');
|
||||
|
||||
if (t.isJSXMemberExpression(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tag = name.get('name').node;
|
||||
|
||||
return !htmlTags.includes(tag) && !svgTags.includes(tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* Transform vModel
|
||||
*/
|
||||
const getModelDirective = (t, path, value) => {
|
||||
const tag = getTagName(path);
|
||||
const type = getType(t, path);
|
||||
|
||||
addProp(path, t.jsxSpreadAttribute(
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.stringLiteral('onUpdate:modelValue'),
|
||||
t.arrowFunctionExpression(
|
||||
[t.identifier('$event')],
|
||||
t.assignmentExpression('=', value, t.identifier('$event')),
|
||||
),
|
||||
),
|
||||
]),
|
||||
));
|
||||
|
||||
if (isComponent(t, path)) {
|
||||
addProp(path, t.jsxAttribute(t.jsxIdentifier('modelValue'), t.jsxExpressionContainer(value)));
|
||||
return null;
|
||||
}
|
||||
|
||||
let modelToUse;
|
||||
switch (tag) {
|
||||
case 'select':
|
||||
if (!path.vueVModelSelect) {
|
||||
path.vueVModelSelect = addNamed(path, 'vModelSelect', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelSelect;
|
||||
break;
|
||||
case 'textarea':
|
||||
if (!path.vueVModelText) {
|
||||
path.vueVModelText = addNamed(path, 'vModelText', 'vue');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
switch (type) {
|
||||
case 'checkbox':
|
||||
if (!path.vueVModelCheckbox) {
|
||||
path.vueVModelCheckbox = addNamed(path, 'vModelCheckbox', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelCheckbox;
|
||||
break;
|
||||
case 'radio':
|
||||
if (!path.vueVModelRadio) {
|
||||
path.vueVModelRadio = addNamed(path, 'vModelRadio', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelRadio;
|
||||
break;
|
||||
default:
|
||||
if (!path.vueVModelText) {
|
||||
path.vueVModelText = addNamed(path, 'vModelText', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelText;
|
||||
}
|
||||
}
|
||||
|
||||
return modelToUse;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parse vModel metadata
|
||||
*
|
||||
* @param t
|
||||
* @param path JSXAttribute
|
||||
* @returns null | Object<{ modifiers: Set<string>, valuePath: Path<Expression>}>
|
||||
*/
|
||||
const parseVModel = (t, path) => {
|
||||
if (t.isJSXNamespacedName(path.get('name')) || !startsWithCamel(path.get('name.name').node, 'v-model')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!t.isJSXExpressionContainer(path.get('value'))) {
|
||||
throw new Error('You have to use JSX Expression inside your v-model');
|
||||
}
|
||||
|
||||
const modifiers = path.get('name.name').node.split('_');
|
||||
|
||||
return {
|
||||
modifiers: new Set(modifiers),
|
||||
value: path.get('value.expression').node,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (t) => ({
|
||||
JSXAttribute: {
|
||||
exit(path) {
|
||||
const parsed = parseVModel(t, path);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { modifiers, value } = parsed;
|
||||
|
||||
const parent = path.parentPath;
|
||||
// v-model={xx} --> v-_model={[directive, xx, void 0, { a: true, b: true }]}
|
||||
const directive = getModelDirective(t, parent, value);
|
||||
if (directive) {
|
||||
path.replaceWith(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier('v-_model'), // TODO
|
||||
t.jsxExpressionContainer(
|
||||
t.arrayExpression([
|
||||
directive,
|
||||
value,
|
||||
modifiers.size && t.unaryExpression('void', t.numericLiteral(0), true),
|
||||
modifiers.size && t.objectExpression(
|
||||
[...modifiers].map(
|
||||
(modifier) => t.objectProperty(
|
||||
t.identifier(modifier),
|
||||
t.booleanLiteral(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
].filter(Boolean)),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
path.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
338
packages/babel-plugin-jsx/src/transform-vue-jsx.js
Normal file
338
packages/babel-plugin-jsx/src/transform-vue-jsx.js
Normal file
@ -0,0 +1,338 @@
|
||||
const htmlTags = require('html-tags');
|
||||
const svgTags = require('svg-tags');
|
||||
const { addNamed, addDefault } = require('@babel/helper-module-imports');
|
||||
|
||||
const xlinkRE = /^xlink([A-Z])/;
|
||||
const eventRE = /^on[A-Z][a-z]+$/;
|
||||
const rootAttributes = ['class', 'style'];
|
||||
|
||||
|
||||
/**
|
||||
* Checks if string is describing a directive
|
||||
* @param src string
|
||||
*/
|
||||
const isDirective = (src) => src.startsWith('v-')
|
||||
|| (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z');
|
||||
|
||||
/**
|
||||
* Transform JSXMemberExpression to MemberExpression
|
||||
* @param t
|
||||
* @param path JSXMemberExpression
|
||||
* @returns MemberExpression
|
||||
*/
|
||||
const transformJSXMemberExpression = (t, path) => {
|
||||
const objectPath = path.get('object');
|
||||
const propertyPath = path.get('property');
|
||||
|
||||
const transformedObject = objectPath.isJSXMemberExpression()
|
||||
? transformJSXMemberExpression(t, 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 t
|
||||
* @param path JSXOpeningElement
|
||||
* @returns Identifier | StringLiteral | MemberExpression
|
||||
*/
|
||||
const getTag = (t, 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(t, namePath);
|
||||
}
|
||||
throw new Error(`getTag: ${namePath.type} is not supported`);
|
||||
};
|
||||
|
||||
const getJSXAttributeName = (t, path) => {
|
||||
const nameNode = path.node.name;
|
||||
if (t.isJSXIdentifier(nameNode)) {
|
||||
return nameNode.name;
|
||||
}
|
||||
|
||||
return `${nameNode.namespace.name}:${nameNode.name.name}`;
|
||||
};
|
||||
|
||||
const getJSXAttributeValue = (t, path) => {
|
||||
const valuePath = path.get('value');
|
||||
if (valuePath.isJSXElement()) {
|
||||
return transformJSXElement(t, valuePath);
|
||||
}
|
||||
if (valuePath.isStringLiteral()) {
|
||||
return valuePath.node;
|
||||
}
|
||||
if (valuePath.isJSXExpressionContainer()) {
|
||||
return transformJSXExpressionContainer(valuePath);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const transformJSXAttribute = (t, path, state, attributesToMerge, directives) => {
|
||||
let name = getJSXAttributeName(t, path);
|
||||
const attributeValue = getJSXAttributeValue(t, path);
|
||||
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
|
||||
const transformOn = addDefault(path, '@ant-design-vue/babel-helper-vue-transform-on', { nameHint: '_transformOn' });
|
||||
attributesToMerge.push(t.callExpression(
|
||||
transformOn,
|
||||
[attributeValue || t.booleanLiteral(true)],
|
||||
));
|
||||
return null;
|
||||
}
|
||||
if (isDirective(name)) {
|
||||
const directiveName = name.startsWith('v-')
|
||||
? name.replace('v-', '')
|
||||
: name.replace(`v${name[1]}`, name[1].toLowerCase());
|
||||
if (directiveName === '_model') {
|
||||
directives.push(attributeValue);
|
||||
} else if (directiveName === 'show') {
|
||||
directives.push(t.arrayExpression([
|
||||
state.vShow,
|
||||
attributeValue,
|
||||
]));
|
||||
} else {
|
||||
directives.push(t.arrayExpression([
|
||||
t.callExpression(state.resolveDirective, [
|
||||
t.stringLiteral(directiveName),
|
||||
]),
|
||||
attributeValue,
|
||||
]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (rootAttributes.includes(name) || eventRE.test(name)) {
|
||||
attributesToMerge.push(
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.stringLiteral(
|
||||
name,
|
||||
),
|
||||
attributeValue,
|
||||
),
|
||||
]),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (name.match(xlinkRE)) {
|
||||
name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
return t.objectProperty(
|
||||
t.stringLiteral(
|
||||
name,
|
||||
),
|
||||
attributeValue || t.booleanLiteral(true),
|
||||
);
|
||||
};
|
||||
|
||||
const transformJSXSpreadAttribute = (t, 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 = (t, path, state, attributesToMerge, directives) => (
|
||||
path.isJSXAttribute()
|
||||
? transformJSXAttribute(t, path, state, attributesToMerge, directives)
|
||||
: transformJSXSpreadAttribute(t, path, attributesToMerge));
|
||||
|
||||
const getAttributes = (t, path, state, directives) => {
|
||||
const attributes = path.get('openingElement').get('attributes');
|
||||
if (attributes.length === 0) {
|
||||
return t.nullLiteral();
|
||||
}
|
||||
|
||||
const attributesToMerge = [];
|
||||
const attributeArray = [];
|
||||
attributes
|
||||
.forEach((attribute) => {
|
||||
const attr = transformAttribute(t, attribute, state, attributesToMerge, directives);
|
||||
if (attr) {
|
||||
attributeArray.push(attr);
|
||||
}
|
||||
});
|
||||
return t.callExpression(
|
||||
state.mergeProps,
|
||||
[
|
||||
...attributesToMerge,
|
||||
t.objectExpression(attributeArray),
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform JSXText to StringLiteral
|
||||
* @param t
|
||||
* @param path JSXText
|
||||
* @returns StringLiteral
|
||||
*/
|
||||
const transformJSXText = (t, 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 t
|
||||
* @param path JSXSpreadChild
|
||||
* @returns SpreadElement
|
||||
*/
|
||||
const transformJSXSpreadChild = (t, path) => t.spreadElement(path.get('expression').node);
|
||||
|
||||
/**
|
||||
* Get children from Array of JSX children
|
||||
* @param t
|
||||
* @param paths Array<JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement>
|
||||
* @returns Array<Expression | SpreadElement>
|
||||
*/
|
||||
const getChildren = (t, paths) => paths
|
||||
.map((path) => {
|
||||
if (path.isJSXText()) {
|
||||
return transformJSXText(t, path);
|
||||
}
|
||||
if (path.isJSXExpressionContainer()) {
|
||||
return transformJSXExpressionContainer(path);
|
||||
}
|
||||
if (path.isJSXSpreadChild()) {
|
||||
return transformJSXSpreadChild(t, path);
|
||||
}
|
||||
if (path.isCallExpression()) {
|
||||
return path.node;
|
||||
}
|
||||
if (path.isJSXElement()) {
|
||||
return transformJSXElement(t, path);
|
||||
}
|
||||
throw new Error(`getChildren: ${path.type} is not supported`);
|
||||
}).filter((value) => (
|
||||
value !== undefined
|
||||
&& value !== null
|
||||
&& !t.isJSXEmptyExpression(value)
|
||||
));
|
||||
|
||||
const transformJSXElement = (t, path, state) => {
|
||||
const directives = [];
|
||||
const tag = getTag(t, path);
|
||||
const children = t.arrayExpression(getChildren(t, path.get('children')));
|
||||
const h = t.callExpression(state.h, [
|
||||
tag,
|
||||
getAttributes(t, path, state, directives),
|
||||
!t.isStringLiteral(tag)
|
||||
? t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.callExpression(state.withCtx, [
|
||||
t.arrowFunctionExpression(
|
||||
[],
|
||||
children,
|
||||
),
|
||||
]),
|
||||
),
|
||||
])
|
||||
: children,
|
||||
]);
|
||||
if (!directives.length) {
|
||||
return h;
|
||||
}
|
||||
return t.callExpression(state.withDirectives, [
|
||||
h,
|
||||
t.arrayExpression(directives),
|
||||
]);
|
||||
};
|
||||
|
||||
const imports = [
|
||||
'h', 'mergeProps', 'withDirectives',
|
||||
'resolveDirective', 'vShow', 'withCtx',
|
||||
];
|
||||
|
||||
module.exports = (t) => ({
|
||||
JSXElement: {
|
||||
exit(path, state) {
|
||||
imports.forEach((m) => {
|
||||
if (!state[m]) {
|
||||
state[m] = addNamed(path, m, 'vue');
|
||||
}
|
||||
});
|
||||
path.replaceWith(
|
||||
transformJSXElement(t, path, state),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
244
packages/babel-plugin-jsx/test/index.test.js
Normal file
244
packages/babel-plugin-jsx/test/index.test.js
Normal file
@ -0,0 +1,244 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
test('should render with render function', () => {
|
||||
const wrapper = shallowMount({
|
||||
render() {
|
||||
return <div>123</div>;
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('123');
|
||||
});
|
||||
|
||||
test('should render with setup', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div>123</div>;
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('123');
|
||||
});
|
||||
|
||||
test('Extracts attrs', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div id="hi" dir="ltr" />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.element.id).toBe('hi');
|
||||
expect(wrapper.element.dir).toBe('ltr');
|
||||
});
|
||||
|
||||
test('Binds attrs', () => {
|
||||
const id = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div>{id}</div>;
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('foo');
|
||||
});
|
||||
|
||||
test('should not fallthrough with inheritAttrs: false', () => {
|
||||
const Child = (props) => <div>{props.foo}</div>;
|
||||
|
||||
Child.inheritAttrs = false;
|
||||
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => (
|
||||
<Child class="parent" foo={1} />
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('1');
|
||||
});
|
||||
|
||||
test('Fragment', () => {
|
||||
const Child = () => <div>123</div>;
|
||||
|
||||
Child.inheritAttrs = false;
|
||||
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => (
|
||||
<>
|
||||
<Child />
|
||||
<div>456</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toBe('<div>123</div><div>456</div>');
|
||||
});
|
||||
|
||||
|
||||
test('xlink:href', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <use xlinkHref={'#name'}></use>;
|
||||
},
|
||||
});
|
||||
expect(wrapper.attributes()['xlink:href']).toBe('#name');
|
||||
});
|
||||
|
||||
test('Merge class', () => {
|
||||
const wrapper = shallowMount({
|
||||
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', () => {
|
||||
const a = ['1', '2'];
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div>{[...a]}</div>;
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('12');
|
||||
});
|
||||
|
||||
test('domProps input[value]', () => {
|
||||
const val = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <input type="text" value={val} />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toBe('<input type="text">');
|
||||
});
|
||||
|
||||
test('domProps input[checked]', () => {
|
||||
const val = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <input checked={val} />;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$.subTree.props.checked).toBe(val);
|
||||
});
|
||||
|
||||
test('domProps option[selected]', () => {
|
||||
const val = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
render() {
|
||||
return <option selected={val} />;
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.$.subTree.props.selected).toBe(val);
|
||||
});
|
||||
|
||||
test('domProps video[muted]', () => {
|
||||
const val = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
render() {
|
||||
return <video muted={val} />;
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$.subTree.props.muted).toBe(val);
|
||||
});
|
||||
|
||||
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)', async () => {
|
||||
const calls = [];
|
||||
const data = {
|
||||
id: 'hehe',
|
||||
onClick() {
|
||||
calls.push(3);
|
||||
},
|
||||
innerHTML: 2,
|
||||
class: ['a', 'b'],
|
||||
};
|
||||
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => (
|
||||
<div
|
||||
href="huhu"
|
||||
{...data}
|
||||
class={{ c: true }}
|
||||
onClick={() => calls.push(4)}
|
||||
hook-insert={() => calls.push(2)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.attributes('id')).toBe('hehe');
|
||||
expect(wrapper.attributes('href')).toBe('huhu');
|
||||
expect(wrapper.text()).toBe('2');
|
||||
expect(wrapper.classes()).toEqual(expect.arrayContaining(['a', 'b', 'c']));
|
||||
|
||||
await wrapper.trigger('click');
|
||||
|
||||
expect(calls).toEqual(expect.arrayContaining([3, 4]));
|
||||
});
|
||||
|
||||
test('directive', () => {
|
||||
const calls = [];
|
||||
const customDirective = {
|
||||
mounted() {
|
||||
calls.push(1);
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(({
|
||||
directives: { custom: customDirective },
|
||||
setup() {
|
||||
return () => (
|
||||
<a
|
||||
v-custom={{
|
||||
value: 123,
|
||||
modifiers: { modifier: true },
|
||||
arg: 'arg',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
const node = wrapper.vm.$.subTree;
|
||||
expect(calls).toEqual(expect.arrayContaining([1]));
|
||||
expect(node.dirs).toHaveLength(1);
|
||||
});
|
1
packages/babel-plugin-jsx/test/setup.js
Normal file
1
packages/babel-plugin-jsx/test/setup.js
Normal file
@ -0,0 +1 @@
|
||||
import 'regenerator-runtime/runtime';
|
36
packages/babel-plugin-jsx/webpack.config.js
Normal file
36
packages/babel-plugin-jsx/webpack.config.js
Normal file
@ -0,0 +1,36 @@
|
||||
const path = require('path');
|
||||
|
||||
const jsxInjectionPATH = 'PACKAGE/lib/jsxInjection';
|
||||
|
||||
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/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
[jsxInjectionPATH]: path.resolve(
|
||||
__dirname,
|
||||
'./lib/jsxInjection',
|
||||
),
|
||||
},
|
||||
},
|
||||
devServer: {
|
||||
inline: true,
|
||||
open: true,
|
||||
hot: true,
|
||||
overlay: true,
|
||||
},
|
||||
};
|
Reference in New Issue
Block a user