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' }], 'import/extensions': [2, 'ignorePackages', { ts: 'never' }],
'@typescript-eslint/ban-ts-comment': [0], '@typescript-eslint/ban-ts-comment': [0],
'@typescript-eslint/explicit-module-boundary-types': [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: { settings: {
'import/resolver': { '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/helper-module-imports';
declare module '@babel/plugin-syntax-jsx'; declare module '@babel/plugin-syntax-jsx';

View File

@ -1,14 +1,10 @@
module.exports = { module.exports = {
presets: [ presets: [
[ '@babel/preset-env',
'@babel/env', '@babel/preset-typescript',
{
// modules: 'cjs',
},
],
], ],
plugins: [ plugins: [
/* eslint-disable-next-line global-require */ /* 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 Child = defineComponent({
const obj = { props: ['foo'],
mouseover: () => { setup(props) {
context.emit('mouseover'); return () => <div>{props.foo}</div>;
}, },
click: () => {
context.emit('click');
},
};
return (
<div class={props.class}>
Super
<button
on={obj}
>
{ props.buttonText }
{context.slots.default()}
</button>
</div>
);
};
SuperButton.inheritAttrs = false;
const App = defineComponent(() => {
const count = ref(0);
const inc = () => {
count.value++;
};
const obj = {
click: inc,
mouseover: inc,
};
return () => (
<div>
Foo {count.value}
<SuperButton
buttonText="VueComponent"
class="xxx"
vShow={true}
on={obj}
>
<button>1234</button>
</SuperButton>
</div>
);
}); });
createApp(App).mount('#app'); 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 = { 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": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0", "@babel/preset-env": "^7.0.0",
"@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-babel": "^5.0.3", "@rollup/plugin-babel": "^5.0.3",
"@types/jest": "^26.0.7",
"@types/svg-tags": "^1.0.0", "@types/svg-tags": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^3.6.1", "@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1", "@typescript-eslint/parser": "^3.6.1",
@ -47,6 +49,7 @@
"jest": "^26.0.1", "jest": "^26.0.1",
"regenerator-runtime": "^0.13.5", "regenerator-runtime": "^0.13.5",
"rollup": "^2.13.1", "rollup": "^2.13.1",
"ts-jest": "^26.1.3",
"typescript": "^3.9.6", "typescript": "^3.9.6",
"vue": "3.0.0-rc.4", "vue": "3.0.0-rc.4",
"webpack": "^4.43.0", "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 { interface Opts {
transformOn?: boolean; transformOn?: boolean;
compatibleProps?: boolean; compatibleProps?: boolean;
usePatchFlag?: boolean; optimize?: boolean;
isCustomElement?: (tag: string) => boolean;
} }
export type ExcludesBoolean = <T>(x: T | false | true) => x is T; 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 { createIdentifier } from './utils';
import { State, ExcludesBoolean } from '.'; 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 * Get JSX element type
@ -18,17 +18,17 @@ const getType = (path: NodePath<t.JSXOpeningElement>) => {
return false; return false;
} }
return t.isJSXIdentifier(attribute.get('name')) return t.isJSXIdentifier(attribute.get('name'))
&& (attribute.get('name') as NodePath<t.JSXIdentifier>).get('name') === 'type' && (attribute.get('name') as NodePath<t.JSXIdentifier>).node.name === 'type';
&& t.isStringLiteral(attribute.get('value')); }) as NodePath<t.JSXAttribute> | undefined;
});
return typePath ? typePath.get('value.value') : ''; return typePath ? typePath.get('value').node : null;
}; };
const parseModifiers = (value: t.Expression) => { const parseModifiers = (value: t.Expression) => {
let modifiers: string[] = []; let modifiers: string[] = [];
if (t.isArrayExpression(value)) { 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; return modifiers;
}; };
@ -57,7 +57,8 @@ const parseDirectives = (args: {
throw new Error('You have to use JSX Expression inside your v-model'); 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)) { if (t.isArrayExpression(value)) {
const { elements } = value as t.ArrayExpression; const { elements } = value as t.ArrayExpression;
@ -78,7 +79,7 @@ const parseDirectives = (args: {
modifiers: modifiersSet, modifiers: modifiersSet,
value: val || value, value: val || value,
arg, arg,
directive: hasDirective ? [ directive: shouldResolve ? [
resolveDirective(path, state, tag, directiveName), resolveDirective(path, state, tag, directiveName),
val || value, val || value,
!!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true), !!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
@ -114,15 +115,19 @@ const resolveDirective = (
modelToUse = createIdentifier(state, 'vModelText'); modelToUse = createIdentifier(state, 'vModelText');
break; break;
default: default:
switch (type) { if (t.isStringLiteral(type) || !type) {
case 'checkbox': switch ((type as t.StringLiteral)?.value) {
modelToUse = createIdentifier(state, 'vModelCheckbox'); case 'checkbox':
break; modelToUse = createIdentifier(state, 'vModelCheckbox');
case 'radio': break;
modelToUse = createIdentifier(state, 'vModelRadio'); case 'radio':
break; modelToUse = createIdentifier(state, 'vModelRadio');
default: break;
modelToUse = createIdentifier(state, 'vModelText'); default:
modelToUse = createIdentifier(state, 'vModelText');
}
} else {
modelToUse = createIdentifier(state, 'vModelDynamic');
} }
} }
return modelToUse; return modelToUse;

View File

@ -3,339 +3,15 @@ import { NodePath } from '@babel/traverse';
import { addDefault, addNamespace } from '@babel/helper-module-imports'; import { addDefault, addNamespace } from '@babel/helper-module-imports';
import { import {
createIdentifier, createIdentifier,
isDirective,
checkIsComponent,
transformJSXSpreadChild, transformJSXSpreadChild,
getTag,
getJSXAttributeName,
transformJSXText, transformJSXText,
transformJSXExpressionContainer, transformJSXExpressionContainer,
walksScope, walksScope,
} from './utils'; } from './utils';
import parseDirectives from './parseDirectives'; import buildProps from './buildProps';
import { PatchFlags, PatchFlagNames } from './patchFlags'; import { PatchFlags } from './patchFlags';
import { State, ExcludesBoolean } from '.'; 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 * Get children from Array of JSX children
* @param paths Array<JSXText | JSXExpressionContainer | JSXElement | JSXFragment> * @param paths Array<JSXText | JSXExpressionContainer | JSXElement | JSXFragment>
@ -405,13 +81,7 @@ const transformJSXElement = (
const useOptimate = path.getData('optimize') !== false; const useOptimate = path.getData('optimize') !== false;
const flagNames = Object.keys(PatchFlagNames) const { compatibleProps = false, optimize = false } = state.opts;
.map(Number)
.filter((n) => n > 0 && patchFlag & n)
.map((n) => PatchFlagNames[n])
.join(', ');
const { compatibleProps = false, usePatchFlag = true } = state.opts;
if (compatibleProps && !state.get('compatibleProps')) { if (compatibleProps && !state.get('compatibleProps')) {
state.set('compatibleProps', addDefault( state.set('compatibleProps', addDefault(
path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' }, path, '@ant-design-vue/babel-helper-vue-compatible-props', { nameHint: '_compatibleProps' },
@ -419,7 +89,7 @@ const transformJSXElement = (
} }
// @ts-ignore // @ts-ignore
const createVNode = t.callExpression(createIdentifier(state, usePatchFlag ? 'createVNode' : 'h'), [ const createVNode = t.callExpression(createIdentifier(state, optimize ? 'createVNode' : 'h'), [
tag, tag,
// @ts-ignore // @ts-ignore
compatibleProps ? t.callExpression(state.get('compatibleProps'), [props]) : props, compatibleProps ? t.callExpression(state.get('compatibleProps'), [props]) : props,
@ -432,18 +102,18 @@ const transformJSXElement = (
), ),
...(slots ? ( ...(slots ? (
t.isObjectExpression(slots) t.isObjectExpression(slots)
? (slots as any as t.ObjectExpression).properties ? (slots! as t.ObjectExpression).properties
: [t.spreadElement(slots as any)] : [t.spreadElement(slots!)]
) : []), ) : []),
].filter(Boolean as any as ExcludesBoolean)) ].filter(Boolean as any as ExcludesBoolean))
: t.arrayExpression(children) : t.arrayExpression(children)
) : t.nullLiteral(), ) : t.nullLiteral(),
!!patchFlag && usePatchFlag && ( !!patchFlag && optimize && (
useOptimate useOptimate
? t.addComment(t.numericLiteral(patchFlag), 'trailing', ` ${flagNames} `, false) ? t.numericLiteral(patchFlag)
: t.numericLiteral(PatchFlags.BAIL) : t.numericLiteral(PatchFlags.BAIL)
), ),
!!dynamicPropNames.size && usePatchFlag !!dynamicPropNames.size && optimize
&& t.arrayExpression( && t.arrayExpression(
[...dynamicPropNames.keys()].map((name) => t.stringLiteral(name as string)), [...dynamicPropNames.keys()].map((name) => t.stringLiteral(name as string)),
), ),
@ -459,6 +129,8 @@ const transformJSXElement = (
]); ]);
}; };
export { transformJSXElement };
export default () => ({ export default () => ({
JSXElement: { JSXElement: {
exit(path: NodePath<t.JSXElement>, state: State) { exit(path: NodePath<t.JSXElement>, state: State) {

View File

@ -86,7 +86,11 @@ const getTag = (
if (!htmlTags.includes(name) && !svgTags.includes(name)) { if (!htmlTags.includes(name) && !svgTags.includes(name)) {
return path.scope.hasBinding(name) return path.scope.hasBinding(name)
? t.identifier(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); 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 {
import { shallowMount, mount } from '@vue/test-utils'; 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; const { patchFlag, dynamicProps } = wrapper.vm.$.subTree;
expect(patchFlag).toBe(flag); expect(patchFlag).toBe(flag);
@ -30,11 +36,10 @@ describe('Transform JSX', () => {
test('Extracts attrs', () => { test('Extracts attrs', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div id="hi" dir="ltr" />; return () => <div id="hi" />;
}, },
}); });
expect(wrapper.element.id).toBe('hi'); expect(wrapper.element.id).toBe('hi');
expect(wrapper.element.dir).toBe('ltr');
}); });
test('Binds attrs', () => { test('Binds attrs', () => {
@ -48,13 +53,20 @@ describe('Transform JSX', () => {
}); });
test('should not fallthrough with inheritAttrs: false', () => { 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; Child.inheritAttrs = false;
const wrapper = mount({ const wrapper = mount({
setup() { render() {
return () => ( return (
<Child class="parent" foo={1} /> <Child class="parent" foo={1} />
); );
}, },
@ -83,9 +95,15 @@ describe('Transform JSX', () => {
}); });
test('nested component', () => { 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 />); const wrapper = mount(() => <A.B />);
@ -104,6 +122,7 @@ describe('Transform JSX', () => {
test('Merge class', () => { test('Merge class', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
// @ts-ignore
return () => <div class="a" {...{ class: 'b' } } />; return () => <div class="a" {...{ class: 'b' } } />;
}, },
}); });
@ -114,22 +133,18 @@ describe('Transform JSX', () => {
const propsA = { const propsA = {
style: { style: {
color: 'red', color: 'red',
}, } as CSSProperties,
}; };
const propsB = { const propsB = {
style: [ style: {
{ color: 'blue',
color: 'blue', width: '300px',
width: '200px', height: '300px',
}, } as CSSProperties,
{
width: '300px',
height: '300px',
},
],
}; };
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
// @ts-ignore
return () => <div { ...propsA } { ...propsB } />; return () => <div { ...propsA } { ...propsB } />;
}, },
}); });
@ -157,52 +172,51 @@ describe('Transform JSX', () => {
}); });
test('domProps input[checked]', () => { test('domProps input[checked]', () => {
const val = 'foo'; const val = true;
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <input checked={val} />; return () => <input checked={val} />;
}, },
}); });
expect(wrapper.vm.$.subTree.props.checked).toBe(val); expect(wrapper.vm.$.subTree?.props?.checked).toBe(val);
}); });
test('domProps option[selected]', () => { test('domProps option[selected]', () => {
const val = 'foo'; const val = true;
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <option selected={val} />; return <option selected={val} />;
}, },
}); });
expect(wrapper.vm.$.subTree.props.selected).toBe(val); expect(wrapper.vm.$.subTree?.props?.selected).toBe(val);
}); });
test('domProps video[muted]', () => { test('domProps video[muted]', () => {
const val = 'foo'; const val = true;
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <video muted={val} />; 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)', () => { test('Spread (single object expression)', () => {
const props = { const props = {
innerHTML: 123, id: '1',
other: '1',
}; };
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { 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 () => { test('Spread (mixed)', async () => {
const calls = []; const calls: number[] = [];
const data = { const data = {
id: 'hehe', id: 'hehe',
onClick() { onClick() {
@ -215,7 +229,7 @@ describe('Transform JSX', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => ( return () => (
<div <a
href="huhu" href="huhu"
{...data} {...data}
class={{ c: true }} class={{ c: true }}
@ -235,9 +249,11 @@ describe('Transform JSX', () => {
expect(calls).toEqual(expect.arrayContaining([3, 4])); expect(calls).toEqual(expect.arrayContaining([3, 4]));
}); });
});
test('directive', () => { describe('directive', () => {
const calls = []; test('custom', () => {
const calls: number[] = [];
const customDirective = { const customDirective = {
mounted() { mounted() {
calls.push(1); calls.push(1);
@ -261,25 +277,45 @@ describe('Transform JSX', () => {
expect(calls).toEqual(expect.arrayContaining([1])); expect(calls).toEqual(expect.arrayContaining([1]));
expect(node.dirs).toHaveLength(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', () => { describe('slots', () => {
test('with default', () => { test('with default', () => {
const A = (_, { slots }) => ( const A = defineComponent({
<div> setup(_, { slots }) {
{slots.default()} return () => (
{slots.foo('val')} <div>
</div> {slots.default?.()}
); {slots.foo?.('val')}
</div>
);
},
});
A.inheritAttrs = false; A.inheritAttrs = false;
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
const slots = { return () => <A v-slots={{ foo: (val: string) => val }}><span>default</span></A>;
foo: (val) => val,
};
return () => <A vSlots={slots}><span>default</span></A>;
}, },
}); });
@ -287,20 +323,21 @@ describe('slots', () => {
}); });
test('without default', () => { test('without default', () => {
const A = (_, { slots }) => ( const A = defineComponent({
<div> setup(_, { slots }) {
{slots.foo('foo')} return () => (
</div> <div>
); {slots.foo?.('foo')}
</div>
);
},
});
A.inheritAttrs = false; A.inheritAttrs = false;
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
const slots = { return () => <A v-slots={{ foo: (val: string) => val }} />;
foo: (val) => val,
};
return () => <A vSlots={slots} />;
}, },
}); });
@ -325,7 +362,7 @@ describe('PatchFlags', () => {
const onClick = () => { const onClick = () => {
visible.value = false; 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 () => { describe('variables outside slots', () => {
const A = { interface AProps {
inc: () => void
}
const A = defineComponent<AProps>({
render() { render() {
return this.$slots.default(); return this.$slots.default?.();
}, },
}; });
A.inheritAttrs = false; 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": { "compilerOptions": {
"rootDirs": ["./src"], "rootDirs": ["./src"],
"outDir": "dist", "outDir": "dist",
"downlevelIteration": true "downlevelIteration": true,
"types": ["node", "jest"]
}, },
"include": [ "include": [
"src/**/*", "src/**/*",

View File

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