chore: use typescript to refactor test and fix some bugs(#42)

* style: formate code

* chore: disable no-non-null-assertion

* chore: test vHtml and vText

* chore: set optimize as true in test

* style: fix eslint warning

* chore: v-model test

* fix: v-model has not type should use vModelText

* chore: use typescript to refactor test

* fix: slots test

* feat: support isCustomElement
This commit is contained in:
Amour1688 2020-07-25 22:39:19 +08:00 committed by GitHub
parent 84b006bdd3
commit ebbd992ba0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 722 additions and 491 deletions

View File

@ -30,7 +30,8 @@ module.exports = {
'import/extensions': [2, 'ignorePackages', { ts: 'never' }],
'@typescript-eslint/ban-ts-comment': [0],
'@typescript-eslint/explicit-module-boundary-types': [0],
'@typescript-eslint/no-explicit-any': [0]
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/no-non-null-assertion': [0]
},
settings: {
'import/resolver': {

1
global.d.ts vendored
View File

@ -1,2 +1,3 @@
declare module '*.js';
declare module '@babel/helper-module-imports';
declare module '@babel/plugin-syntax-jsx';

View File

@ -1,14 +1,10 @@
module.exports = {
presets: [
[
'@babel/env',
{
// modules: 'cjs',
},
],
'@babel/preset-env',
'@babel/preset-typescript',
],
plugins: [
/* eslint-disable-next-line global-require */
[require('./dist/index.js'), { transformOn: true }],
[require('./dist/index.js'), { optimize: true, isCustomElement: (tag) => /^x-/.test(tag) }],
],
};

View File

@ -1,53 +1,30 @@
import { createApp, ref, defineComponent } from 'vue';
import { createApp, 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>
);
const Child = defineComponent({
props: ['foo'],
setup(props) {
return () => <div>{props.foo}</div>;
},
});
createApp(App).mount('#app');
Child.inheritAttrs = false;
const App = defineComponent({
data: () => ({
test: '1',
}),
render() {
return (
<>
<input type="radio" value="1" v-model={this.test} name="test" />
<input type="radio" value="2" v-model={this.test} name="test" />
</>
);
},
});
const app = createApp(App);
app.mount('#app');
console.log(app);

View File

@ -1,3 +1,11 @@
module.exports = {
setupFiles: ['./test/setup.js'],
setupFiles: ['./test/setup.ts'],
transform: {
'\\.(ts|tsx)$': 'ts-jest',
},
globals: {
'ts-jest': {
babelConfig: true,
},
},
};

View File

@ -36,7 +36,9 @@
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-babel": "^5.0.3",
"@types/jest": "^26.0.7",
"@types/svg-tags": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
@ -47,6 +49,7 @@
"jest": "^26.0.1",
"regenerator-runtime": "^0.13.5",
"rollup": "^2.13.1",
"ts-jest": "^26.1.3",
"typescript": "^3.9.6",
"vue": "3.0.0-rc.4",
"webpack": "^4.43.0",

View File

@ -0,0 +1,353 @@
import * as t from '@babel/types';
import { NodePath } from '@babel/traverse';
import { addDefault } from '@babel/helper-module-imports';
import {
createIdentifier,
isDirective,
checkIsComponent,
getTag,
getJSXAttributeName,
walksScope,
transformJSXExpressionContainer,
} from './utils';
import parseDirectives from './parseDirectives';
import { PatchFlags } from './patchFlags';
import { State, ExcludesBoolean } from '.';
import { transformJSXElement } from './transform-vue-jsx';
const xlinkRE = /^xlink([A-Z])/;
const onRE = /^on[^a-z]/;
const isOn = (key: string) => onRE.test(key);
export type Slots = t.Identifier | t.ObjectExpression | null;
const getJSXAttributeValue = (
path: NodePath<t.JSXAttribute>,
state: State,
): (
t.StringLiteral | t.Expression | null
) => {
const valuePath = path.get('value');
if (valuePath.isJSXElement()) {
return transformJSXElement(valuePath, state);
}
if (valuePath.isStringLiteral()) {
return valuePath.node;
}
if (valuePath.isJSXExpressionContainer()) {
return transformJSXExpressionContainer(valuePath);
}
return null;
};
const transformJSXSpreadAttribute = (
nodePath: NodePath,
path: NodePath<t.JSXSpreadAttribute>,
mergeArgs: (t.ObjectProperty | t.Expression)[],
) => {
const argument = path.get('argument') as NodePath<t.ObjectExpression>;
const { properties } = argument.node;
if (!properties) {
if (argument.isIdentifier()) {
walksScope(nodePath, (argument.node as t.Identifier).name);
}
mergeArgs.push(argument.node);
} else {
mergeArgs.push(t.objectExpression(properties));
}
};
const mergeAsArray = (existing: t.ObjectProperty, incoming: t.ObjectProperty) => {
if (t.isArrayExpression(existing.value)) {
existing.value.elements.push(incoming.value as t.Expression);
} else {
existing.value = t.arrayExpression([
existing.value as t.Expression,
incoming.value as t.Expression,
]);
}
};
const dedupeProperties = (properties: t.ObjectProperty[] = []) => {
const knownProps = new Map<string, t.ObjectProperty>();
const deduped: t.ObjectProperty[] = [];
properties.forEach((prop) => {
const { value: name } = prop.key as t.StringLiteral;
const existing = knownProps.get(name);
if (existing) {
if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(existing, prop);
}
} else {
knownProps.set(name, prop);
deduped.push(prop);
}
});
return deduped;
};
/**
* Check if an attribute value is constant
* @param node
* @returns boolean
*/
const isConstant = (
node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null,
): boolean => {
if (t.isIdentifier(node)) {
return node.name === 'undefined';
}
if (t.isArrayExpression(node)) {
const { elements } = node;
return elements.every((element) => element && isConstant(element));
}
if (t.isObjectExpression(node)) {
return node.properties.every((property) => isConstant((property as any).value));
}
if (t.isLiteral(node)) {
return true;
}
return false;
};
const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
const tag = getTag(path, state);
const isComponent = checkIsComponent(path.get('openingElement'));
const props = path.get('openingElement').get('attributes');
const directives: t.ArrayExpression[] = [];
const dynamicPropNames = new Set();
let slots: Slots = null;
let patchFlag = 0;
if (props.length === 0) {
return {
tag,
isComponent,
slots,
props: t.nullLiteral(),
directives,
patchFlag,
dynamicPropNames,
};
}
const properties: t.ObjectProperty[] = [];
// patchFlag analysis
let hasRef = false;
let hasClassBinding = false;
let hasStyleBinding = false;
let hasHydrationEventBinding = false;
let hasDynamicKeys = false;
const mergeArgs: (t.CallExpression | t.ObjectProperty | t.Identifier)[] = [];
props
.forEach((prop) => {
if (prop.isJSXAttribute()) {
let name = getJSXAttributeName(prop);
const attributeValue = getJSXAttributeValue(prop, state);
if (!isConstant(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.add(name);
}
}
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
if (!state.get('transformOn')) {
state.set('transformOn', addDefault(
path,
'@ant-design-vue/babel-helper-vue-transform-on',
{ nameHint: '_transformOn' },
));
}
mergeArgs.push(t.callExpression(
state.get('transformOn'),
[attributeValue || t.booleanLiteral(true)],
));
return;
}
if (isDirective(name)) {
const {
directive, modifiers, value, arg, directiveName,
} = parseDirectives({
tag,
isComponent,
name,
path: prop,
state,
value: attributeValue,
});
const argVal = (arg as t.StringLiteral)?.value;
const propName = argVal || 'modelValue';
if (directiveName === 'slots') {
slots = attributeValue as Slots;
return;
}
if (directive) {
directives.push(t.arrayExpression(directive));
} else if (directiveName === 'model') {
// must be v-model and is a component
properties.push(t.objectProperty(
arg || t.stringLiteral('modelValue'),
// @ts-ignore
value,
));
dynamicPropNames.add(propName);
if (modifiers.size) {
properties.push(t.objectProperty(
t.stringLiteral(`${argVal || 'model'}Modifiers`),
t.objectExpression(
[...modifiers].map((modifier) => (
t.objectProperty(
t.stringLiteral(modifier),
t.booleanLiteral(true),
)
)),
),
));
}
} else if (directiveName === 'html') {
properties.push(t.objectProperty(
t.stringLiteral('innerHTML'),
value as any,
));
dynamicPropNames.add('innerHTML');
} else if (directiveName === 'text') {
properties.push(t.objectProperty(
t.stringLiteral('textContent'),
value as any,
));
dynamicPropNames.add('textContent');
}
if (directiveName === 'model' && value) {
properties.push(t.objectProperty(
t.stringLiteral(`onUpdate:${propName}`),
t.arrowFunctionExpression(
[t.identifier('$event')],
// @ts-ignore
t.assignmentExpression('=', value, t.identifier('$event')),
),
));
dynamicPropNames.add(`onUpdate:${propName}`);
}
return;
}
if (name.match(xlinkRE)) {
name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
}
properties.push(t.objectProperty(
t.stringLiteral(name),
attributeValue || t.booleanLiteral(true),
));
} else {
// JSXSpreadAttribute
hasDynamicKeys = true;
transformJSXSpreadAttribute(
path as NodePath,
prop as NodePath<t.JSXSpreadAttribute>,
mergeArgs,
);
}
});
// patchFlag analysis
// tslint:disable: no-bitwise
if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS;
} else {
if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS;
}
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE;
}
if (dynamicPropNames.size) {
patchFlag |= PatchFlags.PROPS;
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS;
}
}
if (
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS)
&& (hasRef || directives.length > 0)
) {
patchFlag |= PatchFlags.NEED_PATCH;
}
let propsExpression: t.Expression | t.ObjectProperty | t.Literal = t.nullLiteral();
if (mergeArgs.length) {
if (properties.length) {
mergeArgs.push(...dedupeProperties(properties));
}
if (mergeArgs.length > 1) {
const exps: (t.CallExpression | t.Identifier)[] = [];
const objectProperties: t.ObjectProperty[] = [];
mergeArgs.forEach((arg) => {
if (t.isIdentifier(arg) || t.isExpression(arg)) {
exps.push(arg);
} else {
objectProperties.push(arg);
}
});
propsExpression = t.callExpression(
createIdentifier(state, 'mergeProps'),
[
...exps,
!!objectProperties.length && t.objectExpression(objectProperties),
].filter(Boolean as any as ExcludesBoolean),
);
} else {
// single no need for a mergeProps call
propsExpression = mergeArgs[0];
}
} else if (properties.length) {
propsExpression = t.objectExpression(dedupeProperties(properties));
}
return {
tag,
props: propsExpression,
isComponent,
slots,
directives,
patchFlag,
dynamicPropNames,
};
};
export default buildProps;

View File

@ -11,7 +11,8 @@ export type State = {
interface Opts {
transformOn?: boolean;
compatibleProps?: boolean;
usePatchFlag?: boolean;
optimize?: boolean;
isCustomElement?: (tag: string) => boolean;
}
export type ExcludesBoolean = <T>(x: T | false | true) => x is T;

View File

@ -3,7 +3,7 @@ import { NodePath } from '@babel/traverse';
import { createIdentifier } from './utils';
import { State, ExcludesBoolean } from '.';
type Tag = t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression;
export type Tag = t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression;
/**
* Get JSX element type
@ -18,17 +18,17 @@ const getType = (path: NodePath<t.JSXOpeningElement>) => {
return false;
}
return t.isJSXIdentifier(attribute.get('name'))
&& (attribute.get('name') as NodePath<t.JSXIdentifier>).get('name') === 'type'
&& t.isStringLiteral(attribute.get('value'));
});
&& (attribute.get('name') as NodePath<t.JSXIdentifier>).node.name === 'type';
}) as NodePath<t.JSXAttribute> | undefined;
return typePath ? typePath.get('value.value') : '';
return typePath ? typePath.get('value').node : null;
};
const parseModifiers = (value: t.Expression) => {
let modifiers: string[] = [];
if (t.isArrayExpression(value)) {
modifiers = (value as t.ArrayExpression).elements.map((el) => (t.isStringLiteral(el) ? el.value : '')).filter(Boolean);
modifiers = (value as t.ArrayExpression).elements
.map((el) => (t.isStringLiteral(el) ? el.value : '')).filter(Boolean);
}
return modifiers;
};
@ -57,7 +57,8 @@ const parseDirectives = (args: {
throw new Error('You have to use JSX Expression inside your v-model');
}
const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent);
const shouldResolve = !['html', 'text', 'model'].includes(directiveName)
|| (directiveName === 'model' && !isComponent);
if (t.isArrayExpression(value)) {
const { elements } = value as t.ArrayExpression;
@ -78,7 +79,7 @@ const parseDirectives = (args: {
modifiers: modifiersSet,
value: val || value,
arg,
directive: hasDirective ? [
directive: shouldResolve ? [
resolveDirective(path, state, tag, directiveName),
val || value,
!!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
@ -114,15 +115,19 @@ const resolveDirective = (
modelToUse = createIdentifier(state, 'vModelText');
break;
default:
switch (type) {
case 'checkbox':
modelToUse = createIdentifier(state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(state, 'vModelText');
if (t.isStringLiteral(type) || !type) {
switch ((type as t.StringLiteral)?.value) {
case 'checkbox':
modelToUse = createIdentifier(state, 'vModelCheckbox');
break;
case 'radio':
modelToUse = createIdentifier(state, 'vModelRadio');
break;
default:
modelToUse = createIdentifier(state, 'vModelText');
}
} else {
modelToUse = createIdentifier(state, 'vModelDynamic');
}
}
return modelToUse;

View File

@ -3,339 +3,15 @@ import { NodePath } from '@babel/traverse';
import { addDefault, addNamespace } from '@babel/helper-module-imports';
import {
createIdentifier,
isDirective,
checkIsComponent,
transformJSXSpreadChild,
getTag,
getJSXAttributeName,
transformJSXText,
transformJSXExpressionContainer,
walksScope,
} from './utils';
import parseDirectives from './parseDirectives';
import { PatchFlags, PatchFlagNames } from './patchFlags';
import buildProps from './buildProps';
import { PatchFlags } from './patchFlags';
import { State, ExcludesBoolean } from '.';
const xlinkRE = /^xlink([A-Z])/;
const onRE = /^on[^a-z]/;
const isOn = (key: string) => onRE.test(key);
const transformJSXSpreadAttribute = (
nodePath: NodePath,
path: NodePath<t.JSXSpreadAttribute>,
mergeArgs: (t.ObjectProperty | t.Expression)[],
) => {
const argument = path.get('argument') as NodePath<t.ObjectExpression>;
const { properties } = argument.node;
if (!properties) {
if (argument.isIdentifier()) {
walksScope(nodePath, (argument.node as t.Identifier).name);
}
mergeArgs.push(argument.node);
} else {
mergeArgs.push(t.objectExpression(properties));
}
};
const getJSXAttributeValue = (
path: NodePath<t.JSXAttribute>,
state: State,
): (
t.StringLiteral | t.Expression | null
) => {
const valuePath = path.get('value');
if (valuePath.isJSXElement()) {
return transformJSXElement(valuePath, state);
}
if (valuePath.isStringLiteral()) {
return valuePath.node;
}
if (valuePath.isJSXExpressionContainer()) {
return transformJSXExpressionContainer(valuePath);
}
return null;
};
/**
* Check if an attribute value is constant
* @param node
* @returns boolean
*/
const isConstant = (
node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null,
): boolean => {
if (t.isIdentifier(node)) {
return node.name === 'undefined';
}
if (t.isArrayExpression(node)) {
const { elements } = node;
return elements.every((element) => element && isConstant(element));
}
if (t.isObjectExpression(node)) {
return node.properties.every((property) => isConstant((property as any).value));
}
if (t.isLiteral(node)) {
return true;
}
return false;
};
const mergeAsArray = (existing: t.ObjectProperty, incoming: t.ObjectProperty) => {
if (t.isArrayExpression(existing.value)) {
existing.value.elements.push(incoming.value as t.Expression);
} else {
existing.value = t.arrayExpression([
existing.value as t.Expression,
incoming.value as t.Expression,
]);
}
};
const dedupeProperties = (properties: t.ObjectProperty[] = []) => {
const knownProps = new Map<string, t.ObjectProperty>();
const deduped: t.ObjectProperty[] = [];
properties.forEach((prop) => {
const { value: name } = prop.key as t.StringLiteral;
const existing = knownProps.get(name);
if (existing) {
if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(existing, prop);
}
} else {
knownProps.set(name, prop);
deduped.push(prop);
}
});
return deduped;
};
const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
const tag = getTag(path, state);
const isComponent = checkIsComponent(path.get('openingElement'));
const props = path.get('openingElement').get('attributes');
const directives: t.ArrayExpression[] = [];
const dynamicPropNames = new Set();
let slots: t.Identifier | t.Expression | null = null;
let patchFlag = 0;
if (props.length === 0) {
return {
tag,
isComponent,
slots,
props: t.nullLiteral(),
directives,
patchFlag,
dynamicPropNames,
};
}
const properties: t.ObjectProperty[] = [];
// patchFlag analysis
let hasRef = false;
let hasClassBinding = false;
let hasStyleBinding = false;
let hasHydrationEventBinding = false;
let hasDynamicKeys = false;
const mergeArgs: (t.CallExpression | t.ObjectProperty | t.Identifier)[] = [];
props
.forEach((prop) => {
if (prop.isJSXAttribute()) {
let name = getJSXAttributeName(prop);
const attributeValue = getJSXAttributeValue(prop, state);
if (!isConstant(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.add(name);
}
}
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
if (!state.get('transformOn')) {
state.set('transformOn', addDefault(
path,
'@ant-design-vue/babel-helper-vue-transform-on',
{ nameHint: '_transformOn' },
));
}
mergeArgs.push(t.callExpression(
state.get('transformOn'),
[attributeValue || t.booleanLiteral(true)],
));
return;
}
if (isDirective(name)) {
const {
directive, modifiers, value, arg, directiveName,
} = parseDirectives({
tag,
isComponent,
name,
path: prop,
state,
value: attributeValue,
});
const argVal = (arg as t.StringLiteral)?.value;
const propName = argVal || 'modelValue';
if (directiveName === 'slots') {
slots = attributeValue;
return;
} if (directive) {
directives.push(t.arrayExpression(directive));
} else {
// must be v-model and is a component
properties.push(t.objectProperty(
arg || t.stringLiteral('modelValue'),
// @ts-ignore
value,
));
dynamicPropNames.add(propName);
if (modifiers.size) {
properties.push(t.objectProperty(
t.stringLiteral(`${argVal || 'model'}Modifiers`),
t.objectExpression(
[...modifiers].map((modifier) => (
t.objectProperty(
t.stringLiteral(modifier),
t.booleanLiteral(true),
)
)),
),
));
}
}
if (directiveName === 'model' && value) {
properties.push(t.objectProperty(
t.stringLiteral(`onUpdate:${propName}`),
t.arrowFunctionExpression(
[t.identifier('$event')],
// @ts-ignore
t.assignmentExpression('=', value, t.identifier('$event')),
),
));
dynamicPropNames.add(`onUpdate:${propName}`);
}
return;
}
if (name.match(xlinkRE)) {
name = name.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`);
}
properties.push(t.objectProperty(
t.stringLiteral(name),
attributeValue || t.booleanLiteral(true),
));
} else {
// JSXSpreadAttribute
hasDynamicKeys = true;
transformJSXSpreadAttribute(
path as NodePath,
prop as NodePath<t.JSXSpreadAttribute>,
mergeArgs,
);
}
});
// patchFlag analysis
// tslint:disable: no-bitwise
if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS;
} else {
if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS;
}
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE;
}
if (dynamicPropNames.size) {
patchFlag |= PatchFlags.PROPS;
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS;
}
}
if (
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS)
&& (hasRef || directives.length > 0)
) {
patchFlag |= PatchFlags.NEED_PATCH;
}
let propsExpression: t.Expression | t.ObjectProperty | t.Literal = t.nullLiteral();
if (mergeArgs.length) {
if (properties.length) {
mergeArgs.push(...dedupeProperties(properties));
}
if (mergeArgs.length > 1) {
const exps: (t.CallExpression | t.Identifier)[] = [];
const objectProperties: t.ObjectProperty[] = [];
mergeArgs.forEach((arg) => {
if (t.isIdentifier(arg) || t.isExpression(arg)) {
exps.push(arg);
} else {
objectProperties.push(arg);
}
});
propsExpression = t.callExpression(
createIdentifier(state, 'mergeProps'),
[
...exps,
!!objectProperties.length && t.objectExpression(objectProperties),
].filter(Boolean as any as ExcludesBoolean),
);
} else {
// single no need for a mergeProps call
propsExpression = mergeArgs[0];
}
} else if (properties.length) {
propsExpression = t.objectExpression(dedupeProperties(properties));
}
return {
tag,
props: propsExpression,
isComponent,
slots,
directives,
patchFlag,
dynamicPropNames,
};
};
/**
* Get children from Array of JSX children
* @param paths Array<JSXText | JSXExpressionContainer | JSXElement | JSXFragment>
@ -405,13 +81,7 @@ const transformJSXElement = (
const useOptimate = path.getData('optimize') !== false;
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter((n) => n > 0 && patchFlag & n)
.map((n) => PatchFlagNames[n])
.join(', ');
const { compatibleProps = false, usePatchFlag = true } = state.opts;
const { compatibleProps = false, optimize = false } = state.opts;
if (compatibleProps && !state.get('compatibleProps')) {
state.set('compatibleProps', addDefault(
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
@ -419,7 +89,7 @@ const transformJSXElement = (
}
// @ts-ignore
const createVNode = t.callExpression(createIdentifier(state, usePatchFlag ? 'createVNode' : 'h'), [
const createVNode = t.callExpression(createIdentifier(state, optimize ? 'createVNode' : 'h'), [
tag,
// @ts-ignore
compatibleProps ? t.callExpression(state.get('compatibleProps'), [props]) : props,
@ -432,18 +102,18 @@ const transformJSXElement = (
),
...(slots ? (
t.isObjectExpression(slots)
? (slots as any as t.ObjectExpression).properties
: [t.spreadElement(slots as any)]
? (slots! as t.ObjectExpression).properties
: [t.spreadElement(slots!)]
) : []),
].filter(Boolean as any as ExcludesBoolean))
: t.arrayExpression(children)
) : t.nullLiteral(),
!!patchFlag && usePatchFlag && (
!!patchFlag && optimize && (
useOptimate
? t.addComment(t.numericLiteral(patchFlag), 'trailing', ` ${flagNames} `, false)
? t.numericLiteral(patchFlag)
: t.numericLiteral(PatchFlags.BAIL)
),
!!dynamicPropNames.size && usePatchFlag
!!dynamicPropNames.size && optimize
&& t.arrayExpression(
[...dynamicPropNames.keys()].map((name) => t.stringLiteral(name as string)),
),
@ -459,6 +129,8 @@ const transformJSXElement = (
]);
};
export { transformJSXElement };
export default () => ({
JSXElement: {
exit(path: NodePath<t.JSXElement>, state: State) {

View File

@ -86,7 +86,11 @@ const getTag = (
if (!htmlTags.includes(name) && !svgTags.includes(name)) {
return path.scope.hasBinding(name)
? t.identifier(name)
: t.callExpression(createIdentifier(state, 'resolveComponent'), [t.stringLiteral(name)]);
: (
state.opts.isCustomElement?.(name)
? t.stringLiteral(name)
: t.callExpression(createIdentifier(state, 'resolveComponent'), [t.stringLiteral(name)])
);
}
return t.stringLiteral(name);

View File

@ -1,13 +0,0 @@
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',
});
});

View File

@ -0,0 +1,16 @@
import * as fs from 'fs';
import * as path from 'path';
import { transformSync } from '@babel/core';
import preset from '../babel.config.js';
test('coverage', () => {
['index.test.tsx', 'v-model.test.tsx']
.forEach((filename) => {
const mainTest = fs.readFileSync(path.resolve(__dirname, `./${filename}`)).toString();
transformSync(mainTest, {
babelrc: false,
presets: [preset],
filename,
});
});
});

View File

@ -1,7 +1,13 @@
import { reactive, ref } from 'vue';
import { shallowMount, mount } from '@vue/test-utils';
import {
reactive, ref, defineComponent, CSSProperties, ComponentPublicInstance,
} from 'vue';
import { shallowMount, mount, VueWrapper } from '@vue/test-utils';
const patchFlagExpect = (wrapper, flag, dynamic) => {
const patchFlagExpect = (
wrapper: VueWrapper<ComponentPublicInstance>,
flag: number,
dynamic: string[] | null,
) => {
const { patchFlag, dynamicProps } = wrapper.vm.$.subTree;
expect(patchFlag).toBe(flag);
@ -30,11 +36,10 @@ describe('Transform JSX', () => {
test('Extracts attrs', () => {
const wrapper = shallowMount({
setup() {
return () => <div id="hi" dir="ltr" />;
return () => <div id="hi" />;
},
});
expect(wrapper.element.id).toBe('hi');
expect(wrapper.element.dir).toBe('ltr');
});
test('Binds attrs', () => {
@ -48,13 +53,20 @@ describe('Transform JSX', () => {
});
test('should not fallthrough with inheritAttrs: false', () => {
const Child = (props) => <div>{props.foo}</div>;
const Child = defineComponent({
props: {
foo: Number,
},
setup(props) {
return () => <div>{props.foo}</div>;
},
});
Child.inheritAttrs = false;
const wrapper = mount({
setup() {
return () => (
render() {
return (
<Child class="parent" foo={1} />
);
},
@ -83,9 +95,15 @@ describe('Transform JSX', () => {
});
test('nested component', () => {
const A = {};
const A = {
B: defineComponent({
setup() {
return () => <div>123</div>;
},
}),
};
A.B = () => <div>123</div>;
A.B.inheritAttrs = false;
const wrapper = mount(() => <A.B />);
@ -104,6 +122,7 @@ describe('Transform JSX', () => {
test('Merge class', () => {
const wrapper = shallowMount({
setup() {
// @ts-ignore
return () => <div class="a" {...{ class: 'b' } } />;
},
});
@ -114,22 +133,18 @@ describe('Transform JSX', () => {
const propsA = {
style: {
color: 'red',
},
} as CSSProperties,
};
const propsB = {
style: [
{
color: 'blue',
width: '200px',
},
{
width: '300px',
height: '300px',
},
],
style: {
color: 'blue',
width: '300px',
height: '300px',
} as CSSProperties,
};
const wrapper = shallowMount({
setup() {
// @ts-ignore
return () => <div { ...propsA } { ...propsB } />;
},
});
@ -157,52 +172,51 @@ describe('Transform JSX', () => {
});
test('domProps input[checked]', () => {
const val = 'foo';
const val = true;
const wrapper = shallowMount({
setup() {
return () => <input checked={val} />;
},
});
expect(wrapper.vm.$.subTree.props.checked).toBe(val);
expect(wrapper.vm.$.subTree?.props?.checked).toBe(val);
});
test('domProps option[selected]', () => {
const val = 'foo';
const val = true;
const wrapper = shallowMount({
render() {
return <option selected={val} />;
},
});
expect(wrapper.vm.$.subTree.props.selected).toBe(val);
expect(wrapper.vm.$.subTree?.props?.selected).toBe(val);
});
test('domProps video[muted]', () => {
const val = 'foo';
const val = true;
const wrapper = shallowMount({
render() {
return <video muted={val} />;
},
});
expect(wrapper.vm.$.subTree.props.muted).toBe(val);
expect(wrapper.vm.$.subTree?.props?.muted).toBe(val);
});
test('Spread (single object expression)', () => {
const props = {
innerHTML: 123,
other: '1',
id: '1',
};
const wrapper = shallowMount({
render() {
return <div {...props}></div>;
return <div {...props}>123</div>;
},
});
expect(wrapper.html()).toBe('<div other="1">123</div>');
expect(wrapper.html()).toBe('<div id="1">123</div>');
});
test('Spread (mixed)', async () => {
const calls = [];
const calls: number[] = [];
const data = {
id: 'hehe',
onClick() {
@ -215,7 +229,7 @@ describe('Transform JSX', () => {
const wrapper = shallowMount({
setup() {
return () => (
<div
<a
href="huhu"
{...data}
class={{ c: true }}
@ -235,9 +249,11 @@ describe('Transform JSX', () => {
expect(calls).toEqual(expect.arrayContaining([3, 4]));
});
});
test('directive', () => {
const calls = [];
describe('directive', () => {
test('custom', () => {
const calls: number[] = [];
const customDirective = {
mounted() {
calls.push(1);
@ -261,25 +277,45 @@ describe('Transform JSX', () => {
expect(calls).toEqual(expect.arrayContaining([1]));
expect(node.dirs).toHaveLength(1);
});
test('vHtml', () => {
const wrapper = shallowMount(({
setup() {
return () => <h1 v-html="<div>foo</div>"></h1>;
},
}));
expect(wrapper.html()).toBe('<h1><div>foo</div></h1>');
});
test('vText', () => {
const text = 'foo';
const wrapper = shallowMount(({
setup() {
return () => <div v-text={text}></div>;
},
}));
expect(wrapper.html()).toBe('<div>foo</div>');
});
});
describe('slots', () => {
test('with default', () => {
const A = (_, { slots }) => (
<div>
{slots.default()}
{slots.foo('val')}
</div>
);
const A = defineComponent({
setup(_, { slots }) {
return () => (
<div>
{slots.default?.()}
{slots.foo?.('val')}
</div>
);
},
});
A.inheritAttrs = false;
const wrapper = mount({
setup() {
const slots = {
foo: (val) => val,
};
return () => <A vSlots={slots}><span>default</span></A>;
return () => <A v-slots={{ foo: (val: string) => val }}><span>default</span></A>;
},
});
@ -287,20 +323,21 @@ describe('slots', () => {
});
test('without default', () => {
const A = (_, { slots }) => (
<div>
{slots.foo('foo')}
</div>
);
const A = defineComponent({
setup(_, { slots }) {
return () => (
<div>
{slots.foo?.('foo')}
</div>
);
},
});
A.inheritAttrs = false;
const wrapper = mount({
setup() {
const slots = {
foo: (val) => val,
};
return () => <A vSlots={slots} />;
return () => <A v-slots={{ foo: (val: string) => val }} />;
},
});
@ -325,7 +362,7 @@ describe('PatchFlags', () => {
const onClick = () => {
visible.value = false;
};
return () => <div vShow={visible.value} onClick={onClick}>NEED_PATCH</div>;
return () => <div v-show={visible.value} onClick={onClick}>NEED_PATCH</div>;
},
});
@ -355,12 +392,15 @@ describe('PatchFlags', () => {
});
});
describe('variables outside slots', async () => {
const A = {
describe('variables outside slots', () => {
interface AProps {
inc: () => void
}
const A = defineComponent<AProps>({
render() {
return this.$slots.default();
return this.$slots.default?.();
},
};
});
A.inheritAttrs = false;

View File

@ -0,0 +1,166 @@
import { shallowMount } from '@vue/test-utils';
import { VNode } from '@vue/runtime-dom';
test('input[type="checkbox"] should work', async () => {
const wrapper = shallowMount({
data() {
return {
test: true,
};
},
render() {
return <input type="checkbox" v-model={this.test} />;
},
});
expect(wrapper.vm.$el.checked).toBe(true);
wrapper.vm.test = false;
await wrapper.vm.$nextTick();
expect(wrapper.vm.$el.checked).toBe(false);
expect(wrapper.vm.test).toBe(false);
await wrapper.trigger('click');
expect(wrapper.vm.$el.checked).toBe(true);
expect(wrapper.vm.test).toBe(true);
});
test('input[type="radio"] should work', async () => {
const wrapper = shallowMount({
data: () => ({
test: '1',
}),
render() {
return (
<>
<input type="radio" value="1" v-model={this.test} name="test" />
<input type="radio" value="2" v-model={this.test} name="test" />
</>
);
},
});
const [a, b] = wrapper.vm.$.subTree.children as VNode[];
expect(a.el!.checked).toBe(true);
wrapper.vm.test = '2';
await wrapper.vm.$nextTick();
expect(a.el!.checked).toBe(false);
expect(b.el!.checked).toBe(true);
await a.el!.click();
expect(a.el!.checked).toBe(true);
expect(b.el!.checked).toBe(false);
expect(wrapper.vm.test).toBe('1');
});
test('select should work with value bindings', async () => {
const wrapper = shallowMount({
data: () => ({
test: 2,
}),
render() {
return (
<select v-model={this.test}>
<option value="1">a</option>
<option value={2}>b</option>
<option value={3}>c</option>
</select>
);
},
});
const el = wrapper.vm.$el;
expect(el.value).toBe('2');
expect(el.children[1].selected).toBe(true);
wrapper.vm.test = 3;
await wrapper.vm.$nextTick();
expect(el.value).toBe('3');
expect(el.children[2].selected).toBe(true);
el.value = '1';
await wrapper.trigger('change');
expect(wrapper.vm.test).toBe('1');
el.value = '2';
await wrapper.trigger('change');
expect(wrapper.vm.test).toBe(2);
});
test('textarea should update value both ways', async () => {
const wrapper = shallowMount({
data: () => ({
test: 'b',
}),
render() {
return <textarea v-model={this.test} />;
},
});
const el = wrapper.vm.$el;
expect(el.value).toBe('b');
wrapper.vm.test = 'a';
await wrapper.vm.$nextTick();
expect(el.value).toBe('a');
el.value = 'c';
await wrapper.trigger('input');
expect(wrapper.vm.test).toBe('c');
});
test('input[type="text"] should update value both ways', async () => {
const wrapper = shallowMount({
data: () => ({
test: 'b',
}),
render() {
return <input v-model={this.test} />;
},
});
const el = wrapper.vm.$el;
expect(el.value).toBe('b');
wrapper.vm.test = 'a';
await wrapper.vm.$nextTick();
expect(el.value).toBe('a');
el.value = 'c';
await wrapper.trigger('input');
expect(wrapper.vm.test).toBe('c');
});
test('input[type="text"] .lazy modifier', async () => {
const wrapper = shallowMount({
data: () => ({
test: 'b',
}),
render() {
return <input v-model={[this.test, ['lazy']]} />;
},
});
const el = wrapper.vm.$el;
expect(el.value).toBe('b');
expect(wrapper.vm.test).toBe('b');
el.value = 'c';
await wrapper.trigger('input');
expect(wrapper.vm.test).toBe('b');
el.value = 'c';
await wrapper.trigger('change');
expect(wrapper.vm.test).toBe('c');
});
test('dynamic type should work', async () => {
const wrapper = shallowMount({
data() {
return {
test: true,
type: 'checkbox',
};
},
render() {
return <input type={this.type} v-model={this.test} />;
},
});
expect(wrapper.vm.$el.checked).toBe(true);
wrapper.vm.test = false;
await wrapper.vm.$nextTick();
expect(wrapper.vm.$el.checked).toBe(false);
});

View File

@ -3,7 +3,8 @@
"compilerOptions": {
"rootDirs": ["./src"],
"outDir": "dist",
"downlevelIteration": true
"downlevelIteration": true,
"types": ["node", "jest"]
},
"include": [
"src/**/*",

View File

@ -4,7 +4,7 @@
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"allowJs": false,
"allowJs": true,
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,