refactor: Dedupe props in an object literal

This commit is contained in:
Amour1688 2020-06-14 16:17:59 +08:00
parent b8aa96eb20
commit dd787c0256
4 changed files with 277 additions and 266 deletions

View File

@ -1,13 +1,11 @@
import syntaxJsx from '@babel/plugin-syntax-jsx';
import tranformVueJSX from './transform-vue-jsx';
import sugarVModel from './sugar-v-model';
import sugarFragment from './sugar-fragment';
export default ({ types: t }) => ({
name: 'babel-plugin-jsx',
inherits: syntaxJsx,
visitor: {
...sugarVModel(t),
...tranformVueJSX(t),
...sugarFragment(t),
},

View File

@ -1,174 +0,0 @@
import camelCase from 'camelcase';
import { addNamespace } from '@babel/helper-module-imports';
import { createIdentifier, checkIsComponent } from './utils';
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 : '';
};
/**
* @param t
* Transform vModel
*/
const getModelDirective = (t, path, state, 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 (checkIsComponent(t, path)) {
addProp(path, t.jsxAttribute(t.jsxIdentifier('modelValue'), t.jsxExpressionContainer(value)));
return null;
}
let modelToUse;
switch (tag) {
case 'select':
modelToUse = createIdentifier(t, state, 'vModelSelect');
break;
case 'textarea':
modelToUse = createIdentifier(t, state, 'vModelText');
break;
default:
switch (type) {
case 'checkbox':
modelToUse = createIdentifier(t, state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(t, state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(t, state, 'vModelText');
}
}
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('_');
modifiers.shift();
return {
modifiers: new Set(modifiers),
value: path.get('value.expression').node,
};
};
export default (t) => ({
JSXAttribute: {
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, state, value);
if (directive) {
path.replaceWith(
t.jsxAttribute(
t.jsxIdentifier('_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();
}
},
},
});

View File

@ -10,11 +10,12 @@ import {
transformJSXText,
transformJSXExpressionContainer,
transformJSXSpreadChild,
parseDirectives,
isFragment,
} from './utils';
const xlinkRE = /^xlink([A-Z])/;
const onRE = /^on[A-Z][a-z]+$/;
const rootAttributes = ['class', 'style'];
const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);
@ -22,28 +23,13 @@ const transformJSXSpreadAttribute = (t, path, mergeArgs) => {
const argument = path.get('argument').node;
const { properties } = argument;
if (!properties) {
return t.spreadElement(argument);
// argument is an Identifier
mergeArgs.push(argument);
} else {
mergeArgs.push(t.objectExpression(properties));
}
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 true;
})));
};
const needToMerge = (name) => rootAttributes.includes(name) || isOn(name);
const getJSXAttributeValue = (t, path) => {
const valuePath = path.get('value');
if (valuePath.isJSXElement()) {
@ -81,28 +67,68 @@ const isConstant = (t, path) => {
return false;
};
const mergeAsArray = (t, existing, incoming) => {
if (t.isArrayExpression(existing.value)) {
existing.value.elements.push(incoming.value);
} else {
existing.value = t.arrayExpression([
existing.value,
incoming.value,
]);
}
};
const dedupeProperties = (t, properties = []) => {
const knownProps = new Map();
const deduped = [];
properties.forEach((prop) => {
const { key: { value: name } = {} } = prop;
const existing = knownProps.get(name);
if (existing) {
if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(t, existing, prop);
}
} else {
knownProps.set(name, prop);
deduped.push(prop);
}
});
return deduped;
};
const buildProps = (t, path, state) => {
const tag = getTag(t, path);
const isComponent = checkIsComponent(t, path.get('openingElement'));
const props = path.get('openingElement').get('attributes');
const directives = [];
const dynamicPropNames = new Set();
let patchFlag = 0;
if (isFragment(t, path.get('openingElement.name'))) {
patchFlag |= PatchFlags.STABLE_FRAGMENT;
}
if (props.length === 0) {
return {
tag,
props: t.nullLiteral(),
directives,
patchFlag,
dynamicPropNames,
};
}
const propsExpression = [];
const properties = [];
// 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
@ -110,10 +136,6 @@ const buildProps = (t, path, state) => {
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') {
@ -139,69 +161,85 @@ const buildProps = (t, path, state) => {
name !== 'key'
&& !isDirective(name)
&& name !== 'on'
&& !dynamicPropNames.includes(name)
) {
dynamicPropNames.push(name);
dynamicPropNames.add(name);
}
}
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
const transformOn = addDefault(
path,
'@ant-design-vue/babel-helper-vue-transform-on',
{ nameHint: '_transformOn' },
);
if (!state.get('transformOn')) {
state.set('transformOn', addDefault(
path,
'@ant-design-vue/babel-helper-vue-transform-on',
{ nameHint: '_transformOn' },
));
}
mergeArgs.push(t.callExpression(
transformOn,
state.get('transformOn'),
[attributeValue || t.booleanLiteral(true)],
));
return;
}
if (isDirective(name) || name === 'onUpdate:modelValue') {
if (name === 'onUpdate:modelValue') {
directives.push(attributeValue);
if (isDirective(name)) {
const { directive, modifiers, directiveName } = parseDirectives(
t, {
tag,
isComponent,
name,
path: prop,
state,
value: attributeValue,
},
);
if (directive) {
directives.push(t.arrayExpression(directive));
} 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,
]));
// must be v-model and is a component
properties.push(t.objectProperty(
t.stringLiteral('modelValue'),
attributeValue,
));
dynamicPropNames.add('modelValue');
if (modifiers.size) {
properties.push(t.objectProperty(
t.stringLiteral('modelModifiers'),
t.objectExpression(
[...modifiers].map((modifier) => (
t.objectProperty(
t.stringLiteral(modifier),
t.booleanLiteral(true),
)
)),
),
));
}
}
return;
}
if (needToMerge(name)) {
mergeArgs.push(
t.objectExpression([
t.objectProperty(
t.stringLiteral(
name,
),
attributeValue,
if (directiveName === 'model') {
properties.push(t.objectProperty(
t.stringLiteral('onUpdate:modelValue'),
t.arrowFunctionExpression(
[t.identifier('$event')],
t.assignmentExpression('=', attributeValue, t.identifier('$event')),
),
]),
);
));
dynamicPropNames.add('onUpdate:modelValue');
}
return;
}
if (name.match(xlinkRE)) {
name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
}
propsExpression.push(t.objectProperty(
properties.push(t.objectProperty(
t.stringLiteral(name),
attributeValue || t.booleanLiteral(true),
));
} else {
hasDynamicKeys = true;
propsExpression.push(transformJSXSpreadAttribute(t, prop, mergeArgs));
transformJSXSpreadAttribute(t, prop, mergeArgs);
}
});
@ -215,7 +253,7 @@ const buildProps = (t, path, state) => {
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE;
}
if (dynamicPropNames.length) {
if (dynamicPropNames.size) {
patchFlag |= PatchFlags.PROPS;
}
if (hasHydrationEventBinding) {
@ -230,14 +268,42 @@ const buildProps = (t, path, state) => {
patchFlag |= PatchFlags.NEED_PATCH;
}
let propsExpression;
if (mergeArgs.length) {
if (properties.length) {
mergeArgs.push(...dedupeProperties(t, properties));
}
if (mergeArgs.length > 1) {
const exps = [];
const objectProperties = [];
mergeArgs.forEach((arg) => {
if (t.isIdentifier(arg) || t.isExpression(arg)) {
exps.push(arg);
} else {
objectProperties.push(arg);
}
});
propsExpression = t.callExpression(
createIdentifier(t, state, 'mergeProps'),
[
...exps,
objectProperties.length
&& t.objectExpression(objectProperties),
].filter(Boolean),
);
} else {
// single no need for a mergeProps call
// eslint-disable-next-line prefer-destructuring
propsExpression = mergeArgs[0];
}
} else if (properties.length) {
propsExpression = t.objectExpression(dedupeProperties(t, properties));
}
return {
props: mergeArgs.length ? t.callExpression(
createIdentifier(t, state, 'mergeProps'),
[
...mergeArgs,
propsExpression.length && t.objectExpression(propsExpression),
].filter(Boolean),
) : t.objectExpression(propsExpression),
tag,
props: propsExpression,
directives,
patchFlag,
dynamicPropNames,
@ -270,19 +336,19 @@ const getChildren = (t, paths) => paths
throw new Error(`getChildren: ${path.type} is not supported`);
}).filter((value) => (
value !== undefined
&& value !== null
&& !t.isJSXEmptyExpression(value)
&& value !== null
&& !t.isJSXEmptyExpression(value)
));
const transformJSXElement = (t, path, state) => {
const tag = getTag(t, path);
const children = t.arrayExpression(getChildren(t, path.get('children')));
const {
tag,
props,
directives,
patchFlag,
dynamicPropNames = [],
dynamicPropNames,
} = buildProps(t, path, state);
const flagNames = Object.keys(PatchFlagNames)
@ -292,12 +358,17 @@ const transformJSXElement = (t, path, state) => {
.join(', ');
const isComponent = checkIsComponent(t, path.get('openingElement'));
const child = children.elements.length === 1 ? children.elements[0] : children;
if (state.opts.compatibleProps && !state.get('compatibleProps')) {
state.set('compatibleProps', addDefault(
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
));
}
const createVNode = t.callExpression(createIdentifier(t, state, 'createVNode'), [
tag,
state.opts.compatibleProps ? t.callExpression(addDefault(
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
), [props]) : props,
children.elements.length
state.opts.compatibleProps ? t.callExpression(state.get('compatibleProps'), [props]) : props,
children.elements[0]
? (
isComponent
? t.objectExpression([
@ -306,16 +377,25 @@ const transformJSXElement = (t, path, state) => {
t.callExpression(createIdentifier(t, state, 'withCtx'), [
t.arrowFunctionExpression(
[],
children,
t.isStringLiteral(child)
? t.callExpression(
createIdentifier(t, state, 'createTextVNode'),
[child],
)
: child,
),
]),
),
t.objectProperty(
t.identifier('_'),
t.numericLiteral(1),
),
])
: children
: child
) : t.nullLiteral(),
patchFlag && t.addComment(t.numericLiteral(patchFlag), 'leading', ` ${flagNames} `),
dynamicPropNames.length
&& t.arrayExpression(dynamicPropNames.map((name) => t.stringLiteral(name))),
patchFlag && t.addComment(t.numericLiteral(patchFlag), 'trailing', ` ${flagNames} `, false),
dynamicPropNames.size
&& t.arrayExpression([...dynamicPropNames.keys()].map((name) => t.stringLiteral(name))),
].filter(Boolean));
if (!directives.length) {

View File

@ -43,6 +43,15 @@ const createIdentifier = (t, state, id) => t.memberExpression(state.get('vue'),
const isDirective = (src) => src.startsWith('v-')
|| (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z');
/**
* Check if a JSXOpeningElement is fragment
* @param {*} t
* @param {*} path
* @returns boolean
*/
const isFragment = (t, path) => t.isJSXMemberExpression(path)
&& path.node.property.name;
/**
* Check if a JSXOpeningElement is a component
*
@ -54,7 +63,7 @@ const checkIsComponent = (t, path) => {
const namePath = path.get('name');
if (t.isJSXMemberExpression(namePath)) {
return namePath.node.property.name !== 'Fragment'; // For withCtx
return !isFragment(t, namePath); // For withCtx
}
const tag = namePath.get('name').node;
@ -180,6 +189,102 @@ const transformJSXExpressionContainer = (path) => path.get('expression').node;
*/
const transformJSXSpreadChild = (t, path) => t.spreadElement(path.get('expression').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 : '';
};
const resolveDirective = (t, path, state, tag, directiveName) => {
if (directiveName === 'show') {
return createIdentifier(t, state, 'vShow');
} if (directiveName === 'model') {
let modelToUse;
const type = getType(t, path.parentPath);
switch (tag.value) {
case 'select':
modelToUse = createIdentifier(t, state, 'vModelSelect');
break;
case 'textarea':
modelToUse = createIdentifier(t, state, 'vModelText');
break;
default:
switch (type) {
case 'checkbox':
modelToUse = createIdentifier(t, state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(t, state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(t, state, 'vModelText');
}
}
return modelToUse;
}
return t.callExpression(
createIdentifier(t, state, 'resolveDirective'), [
t.stringLiteral(directiveName),
],
);
};
/**
* Parse directives metadata
*
* @param t
* @param path JSXAttribute
* @returns null | Object<{ modifiers: Set<string>, valuePath: Path<Expression>}>
*/
const parseDirectives = (t, {
name, path, value, state, tag, isComponent,
}) => {
const modifiers = name.split('_');
const directiveName = modifiers.shift()
.replace(/^v/, '')
.replace(/^-/, '')
.replace(/^\S/, (s) => s.toLowerCase());
if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) {
throw new Error('You have to use JSX Expression inside your v-model');
}
const modifiersSet = new Set(modifiers);
const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent);
return {
directiveName,
modifiers: new Set(modifiers),
directive: hasDirective ? [
resolveDirective(t, path, state, tag, directiveName),
value,
modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
modifiersSet.size && t.objectExpression(
[...modifiersSet].map(
(modifier) => t.objectProperty(
t.identifier(modifier),
t.booleanLiteral(true),
),
),
),
].filter(Boolean) : undefined,
};
};
export {
createIdentifier,
@ -193,4 +298,6 @@ export {
transformJSXExpressionContainer,
PatchFlags,
PatchFlagNames,
parseDirectives,
isFragment,
};