mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2024-11-10 09:39:14 +08:00
perf: replace h with createVNode for PatchFlags (#6)
* chore: disable `no-bitwise` in eslint * feat: replace h with createVNode for PatchFlags * fix: vModel modifiers shift * chore: rename v-_model to _model in sugar-v-model * fix: hasRef will not always be false * feat: Check if an attribute value is constant * chore: pass null when children is empty * chore: describe Transform JSX in test * chore: add describe Patch Flags * perf: import compatibleProps when opts.compatibleProps is true * test: add coverage report (#7) * refactor: cjs to esModule Co-authored-by: Haoqun Jiang <haoqunjiang@gmail.com>
This commit is contained in:
parent
88bf7cca93
commit
4c34cf1d5d
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
dist
|
@ -19,5 +19,7 @@ module.exports = {
|
||||
'no-use-before-define': [0],
|
||||
'no-plusplus': [0],
|
||||
'import/no-extraneous-dependencies': [0],
|
||||
'consistent-return': [0],
|
||||
'no-bitwise': [0]
|
||||
},
|
||||
};
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -102,3 +102,5 @@ dist
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
dist
|
||||
|
@ -3,11 +3,12 @@ module.exports = {
|
||||
[
|
||||
'@babel/env',
|
||||
{
|
||||
// "modules": "cjs"
|
||||
// modules: 'cjs',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
['./src/index.js', { transformOn: true }],
|
||||
/* eslint-disable-next-line global-require */
|
||||
[require('./dist/index.js'), { transformOn: true }],
|
||||
],
|
||||
};
|
||||
|
@ -11,9 +11,10 @@
|
||||
"url": "git+https://github.com/vueComponent/jsx.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server",
|
||||
"dev": "npm run build && webpack-dev-server",
|
||||
"build": "rollup -c",
|
||||
"lint": "eslint --ext .js src",
|
||||
"test": "jest"
|
||||
"test": "jest --coverage"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vueComponent/jsx/issues"
|
||||
@ -30,12 +31,14 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@rollup/plugin-babel": "^5.0.3",
|
||||
"@vue/compiler-dom": "^3.0.0-beta.14",
|
||||
"@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",
|
||||
"rollup": "^2.13.1",
|
||||
"vue": "^3.0.0-beta.14",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
|
26
packages/babel-plugin-jsx/rollup.config.js
Normal file
26
packages/babel-plugin-jsx/rollup.config.js
Normal file
@ -0,0 +1,26 @@
|
||||
import babel from '@rollup/plugin-babel';
|
||||
|
||||
export default {
|
||||
input: 'src/index.js',
|
||||
plugins: [
|
||||
babel({
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 8,
|
||||
},
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
},
|
||||
],
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
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');
|
||||
import syntaxJsx from '@babel/plugin-syntax-jsx';
|
||||
import tranformVueJSX from './transform-vue-jsx';
|
||||
import sugarVModel from './sugar-v-model';
|
||||
import sugarFragment from './sugar-fragment';
|
||||
|
||||
module.exports = ({ types: t }) => ({
|
||||
export default ({ types: t }) => ({
|
||||
name: 'babel-plugin-jsx',
|
||||
inherits: syntaxJsx,
|
||||
visitor: {
|
||||
|
@ -1,22 +1,30 @@
|
||||
const helperModuleImports = require('@babel/helper-module-imports');
|
||||
import { addNamespace } from '@babel/helper-module-imports';
|
||||
|
||||
const transformFragment = (t, path, { name }) => {
|
||||
const transformFragment = (t, path, Fragment) => {
|
||||
const children = path.get('children') || [];
|
||||
return t.jsxElement(
|
||||
t.jsxOpeningElement(t.jsxIdentifier(name), []),
|
||||
t.jsxClosingElement(t.jsxIdentifier(name)),
|
||||
t.jsxOpeningElement(Fragment, []),
|
||||
t.jsxClosingElement(Fragment),
|
||||
children.map(({ node }) => node),
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = (t) => ({
|
||||
export default (t) => ({
|
||||
JSXFragment: {
|
||||
enter(path) {
|
||||
if (!path.vueFragment) {
|
||||
path.vueFragment = helperModuleImports.addNamed(path, 'Fragment', 'vue');
|
||||
enter(path, state) {
|
||||
if (!state.get('vue')) {
|
||||
state.set('vue', addNamespace(path, 'vue'));
|
||||
}
|
||||
path.replaceWith(transformFragment(t, path, path.vueFragment));
|
||||
path.replaceWith(
|
||||
transformFragment(
|
||||
t, path,
|
||||
t.jsxMemberExpression(
|
||||
t.jsxIdentifier(state.get('vue').name),
|
||||
t.jsxIdentifier('Fragment'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,8 +1,6 @@
|
||||
const htmlTags = require('html-tags');
|
||||
const svgTags = require('svg-tags');
|
||||
const camelCase = require('camelcase');
|
||||
const { addNamed } = require('@babel/helper-module-imports');
|
||||
|
||||
import camelCase from 'camelcase';
|
||||
import { addNamespace } from '@babel/helper-module-imports';
|
||||
import { createIdentifier, checkIsComponent } from './utils';
|
||||
|
||||
const cachedCamelCase = (() => {
|
||||
const cache = Object.create(null);
|
||||
@ -55,30 +53,11 @@ const getType = (t, path) => {
|
||||
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 getModelDirective = (t, path, state, value) => {
|
||||
const tag = getTagName(path);
|
||||
const type = getType(t, path);
|
||||
|
||||
@ -94,7 +73,7 @@ const getModelDirective = (t, path, value) => {
|
||||
]),
|
||||
));
|
||||
|
||||
if (isComponent(t, path)) {
|
||||
if (checkIsComponent(t, path)) {
|
||||
addProp(path, t.jsxAttribute(t.jsxIdentifier('modelValue'), t.jsxExpressionContainer(value)));
|
||||
return null;
|
||||
}
|
||||
@ -102,35 +81,21 @@ const getModelDirective = (t, path, value) => {
|
||||
let modelToUse;
|
||||
switch (tag) {
|
||||
case 'select':
|
||||
if (!path.vueVModelSelect) {
|
||||
path.vueVModelSelect = addNamed(path, 'vModelSelect', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelSelect;
|
||||
modelToUse = createIdentifier(t, state, 'vModelSelect');
|
||||
break;
|
||||
case 'textarea':
|
||||
if (!path.vueVModelText) {
|
||||
path.vueVModelText = addNamed(path, 'vModelText', 'vue');
|
||||
}
|
||||
modelToUse = createIdentifier(t, state, 'vModelText');
|
||||
break;
|
||||
default:
|
||||
switch (type) {
|
||||
case 'checkbox':
|
||||
if (!path.vueVModelCheckbox) {
|
||||
path.vueVModelCheckbox = addNamed(path, 'vModelCheckbox', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelCheckbox;
|
||||
modelToUse = createIdentifier(t, state, 'vModelCheckbox');
|
||||
break;
|
||||
case 'radio':
|
||||
if (!path.vueVModelRadio) {
|
||||
path.vueVModelRadio = addNamed(path, 'vModelRadio', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelRadio;
|
||||
modelToUse = createIdentifier(t, state, 'vModelRadio');
|
||||
break;
|
||||
default:
|
||||
if (!path.vueVModelText) {
|
||||
path.vueVModelText = addNamed(path, 'vModelText', 'vue');
|
||||
}
|
||||
modelToUse = path.vueVModelText;
|
||||
modelToUse = createIdentifier(t, state, 'vModelText');
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +120,7 @@ const parseVModel = (t, path) => {
|
||||
}
|
||||
|
||||
const modifiers = path.get('name.name').node.split('_');
|
||||
modifiers.shift();
|
||||
|
||||
return {
|
||||
modifiers: new Set(modifiers),
|
||||
@ -162,23 +128,27 @@ const parseVModel = (t, path) => {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (t) => ({
|
||||
export default (t) => ({
|
||||
JSXAttribute: {
|
||||
exit(path) {
|
||||
exit(path, state) {
|
||||
const parsed = parseVModel(t, path);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.get('vue')) {
|
||||
state.set('vue', addNamespace(path, 'vue'));
|
||||
}
|
||||
|
||||
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);
|
||||
const directive = getModelDirective(t, parent, state, value);
|
||||
if (directive) {
|
||||
path.replaceWith(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier('v-_model'), // TODO
|
||||
t.jsxIdentifier('_model'), // TODO
|
||||
t.jsxExpressionContainer(
|
||||
t.arrayExpression([
|
||||
directive,
|
||||
|
@ -1,69 +1,48 @@
|
||||
const htmlTags = require('html-tags');
|
||||
const svgTags = require('svg-tags');
|
||||
const { addNamed, addDefault } = require('@babel/helper-module-imports');
|
||||
import { addDefault, addNamespace } from '@babel/helper-module-imports';
|
||||
import {
|
||||
createIdentifier,
|
||||
PatchFlags,
|
||||
PatchFlagNames,
|
||||
isDirective,
|
||||
checkIsComponent,
|
||||
getTag,
|
||||
getJSXAttributeName,
|
||||
transformJSXText,
|
||||
transformJSXExpressionContainer,
|
||||
transformJSXSpreadChild,
|
||||
} from './utils';
|
||||
|
||||
const xlinkRE = /^xlink([A-Z])/;
|
||||
const eventRE = /^on[A-Z][a-z]+$/;
|
||||
const onRE = /^on[A-Z][a-z]+$/;
|
||||
const rootAttributes = ['class', 'style'];
|
||||
|
||||
const isOn = (key) => onRE.test(key);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const transformJSXSpreadAttribute = (t, path, mergeArgs) => {
|
||||
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)) {
|
||||
mergeArgs.push(
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.stringLiteral(name),
|
||||
value,
|
||||
),
|
||||
]),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return t.stringLiteral(name);
|
||||
}
|
||||
|
||||
if (namePath.isJSXMemberExpression()) {
|
||||
return transformJSXMemberExpression(t, namePath);
|
||||
}
|
||||
throw new Error(`getTag: ${namePath.type} is not supported`);
|
||||
return true;
|
||||
})));
|
||||
};
|
||||
|
||||
const getJSXAttributeName = (t, path) => {
|
||||
const nameNode = path.node.name;
|
||||
if (t.isJSXIdentifier(nameNode)) {
|
||||
return nameNode.name;
|
||||
}
|
||||
|
||||
return `${nameNode.namespace.name}:${nameNode.name.name}`;
|
||||
};
|
||||
const needToMerge = (name) => rootAttributes.includes(name) || isOn(name);
|
||||
|
||||
const getJSXAttributeValue = (t, path) => {
|
||||
const valuePath = path.get('value');
|
||||
@ -80,183 +59,191 @@ const getJSXAttributeValue = (t, path) => {
|
||||
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;
|
||||
/**
|
||||
* Check if an attribute value is constant
|
||||
* @param t
|
||||
* @param path
|
||||
* @returns boolean
|
||||
*/
|
||||
const isConstant = (t, path) => {
|
||||
if (t.isIdentifier(path)) {
|
||||
return path.name === 'undefined';
|
||||
}
|
||||
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 (t.isArrayExpression(path)) {
|
||||
return path.elements.every((element) => isConstant(t, element));
|
||||
}
|
||||
if (rootAttributes.includes(name) || eventRE.test(name)) {
|
||||
attributesToMerge.push(
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.stringLiteral(
|
||||
name,
|
||||
),
|
||||
attributeValue,
|
||||
),
|
||||
]),
|
||||
);
|
||||
return null;
|
||||
if (t.isObjectExpression(path)) {
|
||||
return path.properties.every((property) => isConstant(t, property.value));
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (t.isLiteral(path)) {
|
||||
return true;
|
||||
})));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
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 buildProps = (t, path, state) => {
|
||||
const isComponent = checkIsComponent(t, path.get('openingElement'));
|
||||
const props = path.get('openingElement').get('attributes');
|
||||
const directives = [];
|
||||
if (props.length === 0) {
|
||||
return {
|
||||
props: t.nullLiteral(),
|
||||
directives,
|
||||
};
|
||||
}
|
||||
|
||||
const attributesToMerge = [];
|
||||
const attributeArray = [];
|
||||
attributes
|
||||
.forEach((attribute) => {
|
||||
const attr = transformAttribute(t, attribute, state, attributesToMerge, directives);
|
||||
if (attr) {
|
||||
attributeArray.push(attr);
|
||||
const propsExpression = [];
|
||||
|
||||
// patchFlag analysis
|
||||
let patchFlag = 0;
|
||||
let hasRef = false;
|
||||
let hasClassBinding = false;
|
||||
let hasStyleBinding = false;
|
||||
let hasHydrationEventBinding = false;
|
||||
let hasDynamicKeys = false;
|
||||
|
||||
const dynamicPropNames = [];
|
||||
const mergeArgs = [];
|
||||
|
||||
props
|
||||
.forEach((prop) => {
|
||||
if (prop.isJSXAttribute()) {
|
||||
let name = getJSXAttributeName(t, prop);
|
||||
|
||||
if (name === '_model') {
|
||||
name = 'onUpdate:modelValue';
|
||||
}
|
||||
|
||||
const attributeValue = getJSXAttributeValue(t, prop);
|
||||
|
||||
if (!isConstant(t, attributeValue) || name === 'ref') {
|
||||
if (
|
||||
!isComponent
|
||||
&& isOn(name)
|
||||
// omit the flag for click handlers becaues hydration gives click
|
||||
// dedicated fast path.
|
||||
&& name.toLowerCase() !== 'onclick'
|
||||
// omit v-model handlers
|
||||
&& name !== 'onUpdate:modelValue'
|
||||
) {
|
||||
hasHydrationEventBinding = true;
|
||||
}
|
||||
|
||||
if (name === 'ref') {
|
||||
hasRef = true;
|
||||
} else if (name === 'class' && !isComponent) {
|
||||
hasClassBinding = true;
|
||||
} else if (name === 'style' && !isComponent) {
|
||||
hasStyleBinding = true;
|
||||
} else if (
|
||||
name !== 'key'
|
||||
&& !isDirective(name)
|
||||
&& name !== 'on'
|
||||
&& !dynamicPropNames.includes(name)
|
||||
) {
|
||||
dynamicPropNames.push(name);
|
||||
}
|
||||
}
|
||||
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
|
||||
const transformOn = addDefault(
|
||||
path,
|
||||
'@ant-design-vue/babel-helper-vue-transform-on',
|
||||
{ nameHint: '_transformOn' },
|
||||
);
|
||||
mergeArgs.push(t.callExpression(
|
||||
transformOn,
|
||||
[attributeValue || t.booleanLiteral(true)],
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (isDirective(name) || name === 'onUpdate:modelValue') {
|
||||
if (name === 'onUpdate:modelValue') {
|
||||
directives.push(attributeValue);
|
||||
} else {
|
||||
const directiveName = name.startsWith('v-')
|
||||
? name.replace('v-', '')
|
||||
: name.replace(`v${name[1]}`, name[1].toLowerCase());
|
||||
if (directiveName === 'show') {
|
||||
directives.push(t.arrayExpression([
|
||||
createIdentifier(t, state, 'vShow'),
|
||||
attributeValue,
|
||||
]));
|
||||
} else {
|
||||
directives.push(t.arrayExpression([
|
||||
t.callExpression(createIdentifier(t, state, 'resolveDirective'), [
|
||||
t.stringLiteral(directiveName),
|
||||
]),
|
||||
attributeValue,
|
||||
]));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (needToMerge(name)) {
|
||||
mergeArgs.push(
|
||||
t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.stringLiteral(
|
||||
name,
|
||||
),
|
||||
attributeValue,
|
||||
),
|
||||
]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (name.match(xlinkRE)) {
|
||||
name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
|
||||
}
|
||||
propsExpression.push(t.objectProperty(
|
||||
t.stringLiteral(name),
|
||||
attributeValue || t.booleanLiteral(true),
|
||||
));
|
||||
} else {
|
||||
hasDynamicKeys = true;
|
||||
propsExpression.push(transformJSXSpreadAttribute(t, prop, mergeArgs));
|
||||
}
|
||||
});
|
||||
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;
|
||||
// patchFlag analysis
|
||||
if (hasDynamicKeys) {
|
||||
patchFlag |= PatchFlags.FULL_PROPS;
|
||||
} else {
|
||||
if (hasClassBinding) {
|
||||
patchFlag |= PatchFlags.CLASS;
|
||||
}
|
||||
if (hasStyleBinding) {
|
||||
patchFlag |= PatchFlags.STYLE;
|
||||
}
|
||||
if (dynamicPropNames.length) {
|
||||
patchFlag |= PatchFlags.PROPS;
|
||||
}
|
||||
if (hasHydrationEventBinding) {
|
||||
patchFlag |= PatchFlags.HYDRATE_EVENTS;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (
|
||||
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS)
|
||||
&& hasRef
|
||||
) {
|
||||
patchFlag |= PatchFlags.NEED_PATCH;
|
||||
}
|
||||
|
||||
return str !== '' ? t.stringLiteral(str) : null;
|
||||
return {
|
||||
props: mergeArgs.length ? t.callExpression(
|
||||
createIdentifier(t, state, 'mergeProps'),
|
||||
[
|
||||
...mergeArgs,
|
||||
propsExpression.length && t.objectExpression(propsExpression),
|
||||
].filter(Boolean),
|
||||
) : t.objectExpression(propsExpression),
|
||||
directives,
|
||||
patchFlag,
|
||||
dynamicPropNames,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -289,50 +276,64 @@ const getChildren = (t, paths) => paths
|
||||
|
||||
|
||||
const transformJSXElement = (t, path, state) => {
|
||||
const directives = [];
|
||||
const tag = getTag(t, path);
|
||||
const children = t.arrayExpression(getChildren(t, path.get('children')));
|
||||
const attributes = getAttributes(t, path, state, directives);
|
||||
const compatibleProps = addDefault(
|
||||
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
|
||||
);
|
||||
const h = t.callExpression(state.h, [
|
||||
const {
|
||||
props,
|
||||
directives,
|
||||
patchFlag,
|
||||
dynamicPropNames = [],
|
||||
} = buildProps(t, path, state);
|
||||
|
||||
const flagNames = Object.keys(PatchFlagNames)
|
||||
.map(Number)
|
||||
.filter((n) => n > 0 && patchFlag & n)
|
||||
.map((n) => PatchFlagNames[n])
|
||||
.join(', ');
|
||||
|
||||
const isComponent = checkIsComponent(t, path.get('openingElement'));
|
||||
const createVNode = t.callExpression(createIdentifier(t, state, 'createVNode'), [
|
||||
tag,
|
||||
state.opts.compatibleProps ? t.callExpression(compatibleProps, [attributes]) : attributes,
|
||||
!t.isStringLiteral(tag) && !tag.name.includes('Fragment')
|
||||
? t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.callExpression(state.withCtx, [
|
||||
t.arrowFunctionExpression(
|
||||
[],
|
||||
children,
|
||||
state.opts.compatibleProps ? t.callExpression(addDefault(
|
||||
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
|
||||
), [props]) : props,
|
||||
children.elements.length
|
||||
? (
|
||||
isComponent
|
||||
? t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('default'),
|
||||
t.callExpression(createIdentifier(t, state, 'withCtx'), [
|
||||
t.arrowFunctionExpression(
|
||||
[],
|
||||
children,
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
])
|
||||
: children,
|
||||
]);
|
||||
])
|
||||
: children
|
||||
) : t.nullLiteral(),
|
||||
patchFlag && t.addComment(t.numericLiteral(patchFlag), 'leading', ` ${flagNames} `),
|
||||
dynamicPropNames.length
|
||||
&& t.arrayExpression(dynamicPropNames.map((name) => t.stringLiteral(name))),
|
||||
].filter(Boolean));
|
||||
|
||||
if (!directives.length) {
|
||||
return h;
|
||||
return createVNode;
|
||||
}
|
||||
return t.callExpression(state.withDirectives, [
|
||||
h,
|
||||
|
||||
return t.callExpression(createIdentifier(t, state, 'withDirectives'), [
|
||||
createVNode,
|
||||
t.arrayExpression(directives),
|
||||
]);
|
||||
};
|
||||
|
||||
const imports = [
|
||||
'h', 'mergeProps', 'withDirectives',
|
||||
'resolveDirective', 'vShow', 'withCtx',
|
||||
];
|
||||
|
||||
module.exports = (t) => ({
|
||||
export default (t) => ({
|
||||
JSXElement: {
|
||||
exit(path, state) {
|
||||
imports.forEach((m) => {
|
||||
state[m] = addNamed(path, m, 'vue');
|
||||
});
|
||||
if (!state.get('vue')) {
|
||||
state.set('vue', addNamespace(path, 'vue'));
|
||||
}
|
||||
path.replaceWith(
|
||||
transformJSXElement(t, path, state),
|
||||
);
|
||||
|
196
packages/babel-plugin-jsx/src/utils.js
Normal file
196
packages/babel-plugin-jsx/src/utils.js
Normal file
@ -0,0 +1,196 @@
|
||||
import htmlTags from 'html-tags';
|
||||
import svgTags from 'svg-tags';
|
||||
|
||||
const PatchFlags = {
|
||||
TEXT: 1,
|
||||
CLASS: 1 << 1,
|
||||
STYLE: 1 << 2,
|
||||
PROPS: 1 << 3,
|
||||
FULL_PROPS: 1 << 4,
|
||||
HYDRATE_EVENTS: 1 << 5,
|
||||
STABLE_FRAGMENT: 1 << 6,
|
||||
KEYED_FRAGMENT: 1 << 7,
|
||||
UNKEYED_FRAGMENT: 1 << 8,
|
||||
NEED_PATCH: 1 << 9,
|
||||
DYNAMIC_SLOTS: 1 << 10,
|
||||
HOISTED: -1,
|
||||
BAIL: -2,
|
||||
};
|
||||
|
||||
// dev only flag -> name mapping
|
||||
const PatchFlagNames = {
|
||||
[PatchFlags.TEXT]: 'TEXT',
|
||||
[PatchFlags.CLASS]: 'CLASS',
|
||||
[PatchFlags.STYLE]: 'STYLE',
|
||||
[PatchFlags.PROPS]: 'PROPS',
|
||||
[PatchFlags.FULL_PROPS]: 'FULL_PROPS',
|
||||
[PatchFlags.HYDRATE_EVENTS]: 'HYDRATE_EVENTS',
|
||||
[PatchFlags.STABLE_FRAGMENT]: 'STABLE_FRAGMENT',
|
||||
[PatchFlags.KEYED_FRAGMENT]: 'KEYED_FRAGMENT',
|
||||
[PatchFlags.UNKEYED_FRAGMENT]: 'UNKEYED_FRAGMENT',
|
||||
[PatchFlags.NEED_PATCH]: 'NEED_PATCH',
|
||||
[PatchFlags.DYNAMIC_SLOTS]: 'DYNAMIC_SLOTS',
|
||||
[PatchFlags.HOISTED]: 'HOISTED',
|
||||
[PatchFlags.BAIL]: 'BAIL',
|
||||
};
|
||||
|
||||
const createIdentifier = (t, state, id) => t.memberExpression(state.get('vue'), t.identifier(id));
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
/**
|
||||
* Check if a JSXOpeningElement is a component
|
||||
*
|
||||
* @param t
|
||||
* @param path JSXOpeningElement
|
||||
* @returns boolean
|
||||
*/
|
||||
const checkIsComponent = (t, path) => {
|
||||
const namePath = path.get('name');
|
||||
|
||||
if (t.isJSXMemberExpression(namePath)) {
|
||||
return namePath.node.property.name !== 'Fragment'; // For withCtx
|
||||
}
|
||||
|
||||
const tag = namePath.get('name').node;
|
||||
|
||||
return !htmlTags.includes(tag) && !svgTags.includes(tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
export {
|
||||
createIdentifier,
|
||||
isDirective,
|
||||
checkIsComponent,
|
||||
transformJSXMemberExpression,
|
||||
getTag,
|
||||
getJSXAttributeName,
|
||||
transformJSXText,
|
||||
transformJSXSpreadChild,
|
||||
transformJSXExpressionContainer,
|
||||
PatchFlags,
|
||||
PatchFlagNames,
|
||||
};
|
13
packages/babel-plugin-jsx/test/coverage.test.js
Normal file
13
packages/babel-plugin-jsx/test/coverage.test.js
Normal file
@ -0,0 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { transformSync } from '@babel/core';
|
||||
import preset from '../babel.config';
|
||||
|
||||
test('coverage', () => {
|
||||
const mainTest = fs.readFileSync(path.resolve(__dirname, './index.test.js'));
|
||||
transformSync(mainTest, {
|
||||
babelrc: false,
|
||||
presets: [preset],
|
||||
filename: 'index.test.js',
|
||||
});
|
||||
});
|
@ -1,244 +1,322 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
|
||||
test('should render with render function', () => {
|
||||
const wrapper = shallowMount({
|
||||
render() {
|
||||
return <div>123</div>;
|
||||
},
|
||||
describe('Transform JSX', () => {
|
||||
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 = mount({
|
||||
setup() {
|
||||
return () => (
|
||||
<Child class="parent" foo={1} />
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toBe('1');
|
||||
});
|
||||
|
||||
test('Fragment', () => {
|
||||
const Child = () => <div>123</div>;
|
||||
|
||||
Child.inheritAttrs = false;
|
||||
|
||||
const wrapper = mount({
|
||||
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);
|
||||
});
|
||||
expect(wrapper.text()).toBe('123');
|
||||
});
|
||||
|
||||
test('should render with setup', () => {
|
||||
const wrapper = shallowMount({
|
||||
setup() {
|
||||
return () => <div>123</div>;
|
||||
describe('Patch Flags', () => {
|
||||
let renders = 0;
|
||||
const Child = {
|
||||
setup(props) {
|
||||
return () => {
|
||||
renders++;
|
||||
return <div>{props.text}</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',
|
||||
it('should render when props change', async () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const count = ref(0);
|
||||
const inc = () => {
|
||||
count.value++;
|
||||
};
|
||||
return () => (
|
||||
<div onClick={inc}>
|
||||
<Child text={count.value} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
});
|
||||
|
||||
expect(renders).toBe(1);
|
||||
await wrapper.trigger('click');
|
||||
expect(renders).toBe(2);
|
||||
});
|
||||
|
||||
it('should not render with static props', async () => {
|
||||
renders = 0;
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const count = ref(0);
|
||||
const inc = () => {
|
||||
count.value++;
|
||||
};
|
||||
return () => (
|
||||
<div onClick={inc}>
|
||||
<Child text={1} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
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(renders).toBe(1);
|
||||
await wrapper.trigger('click');
|
||||
expect(renders).toBe(1);
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$.subTree.props.checked).toBe(val);
|
||||
});
|
||||
it('should not render when props does not change', async () => {
|
||||
renders = 0;
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const count = ref(0);
|
||||
const s = ref('a');
|
||||
const inc = () => {
|
||||
count.value++;
|
||||
};
|
||||
return () => (
|
||||
<div onClick={inc}>
|
||||
<Child text={s.value} />
|
||||
{count.value}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
test('domProps option[selected]', () => {
|
||||
const val = 'foo';
|
||||
const wrapper = shallowMount({
|
||||
render() {
|
||||
return <option selected={val} />;
|
||||
},
|
||||
await wrapper.trigger('click');
|
||||
expect(renders).toBe(1);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
58
yarn.lock
58
yarn.lock
@ -144,7 +144,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.10.1"
|
||||
|
||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.1":
|
||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.1", "@babel/helper-module-imports@^7.7.4":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz#dd331bd45bccc566ce77004e9d05fe17add13876"
|
||||
integrity sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg==
|
||||
@ -1886,6 +1886,23 @@
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@rollup/plugin-babel@^5.0.3":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.0.3.tgz#8d416865b0da79faf14e07c8d233abe0eac0753d"
|
||||
integrity sha512-NlaPf4E6YFxeOCbqc+A2PTkB1BSy3rfKu6EJuQ1MGhMHpTVvMqKi6Rf0DlwtnEsTNK9LueUgsGEgp5Occ4KDVA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.7.4"
|
||||
"@rollup/pluginutils" "^3.0.8"
|
||||
|
||||
"@rollup/pluginutils@^3.0.8":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
|
||||
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
|
||||
dependencies:
|
||||
"@types/estree" "0.0.39"
|
||||
estree-walker "^1.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
||||
@ -1938,6 +1955,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/events@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
@ -4330,11 +4352,21 @@ estraverse@^5.1.0:
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
|
||||
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
|
||||
|
||||
estree-walker@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
|
||||
integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
|
||||
|
||||
estree-walker@^0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.8.1.tgz#6230ce2ec9a5cb03888afcaf295f97d90aa52b79"
|
||||
integrity sha512-H6cJORkqvrNziu0KX2hqOMAlA2CiuAxHeGJXSIoKA/KLv229Dw806J3II6mKTm5xiDX1At1EXCfsOQPB+tMB+g==
|
||||
|
||||
estree-walker@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
|
||||
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
@ -7853,7 +7885,7 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
|
||||
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
||||
@ -8594,6 +8626,28 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rollup-plugin-babel@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz#d15bd259466a9d1accbdb2fe2fff17c52d030acb"
|
||||
integrity sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
rollup-pluginutils "^2.8.1"
|
||||
|
||||
rollup-pluginutils@^2.8.1:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
|
||||
integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
|
||||
dependencies:
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^2.13.1:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.13.1.tgz#06ac5be4f85df0b79f23cdfa90de63e78095a984"
|
||||
integrity sha512-EiICynxIO1DTFmFn+/98gfaqCToK2nbjPjHJLuNvpcwc+P035VrXmJxi3JsOhqkdty+0cOEhJ26ceGTY3UPMPQ==
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.5"
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
|
Loading…
Reference in New Issue
Block a user