mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2024-11-10 09:39:14 +08:00
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:
parent
84b006bdd3
commit
ebbd992ba0
@ -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
1
global.d.ts
vendored
@ -1,2 +1,3 @@
|
||||
declare module '*.js';
|
||||
declare module '@babel/helper-module-imports';
|
||||
declare module '@babel/plugin-syntax-jsx';
|
||||
|
@ -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) }],
|
||||
],
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -1,3 +1,11 @@
|
||||
module.exports = {
|
||||
setupFiles: ['./test/setup.js'],
|
||||
setupFiles: ['./test/setup.ts'],
|
||||
transform: {
|
||||
'\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
babelConfig: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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",
|
||||
|
353
packages/babel-plugin-jsx/src/buildProps.ts
Normal file
353
packages/babel-plugin-jsx/src/buildProps.ts
Normal 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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
16
packages/babel-plugin-jsx/test/coverage.test.ts
Normal file
16
packages/babel-plugin-jsx/test/coverage.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
166
packages/babel-plugin-jsx/test/v-model.test.tsx
Normal file
166
packages/babel-plugin-jsx/test/v-model.test.tsx
Normal 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);
|
||||
});
|
@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"rootDirs": ["./src"],
|
||||
"outDir": "dist",
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"experimentalDecorators": true,
|
||||
|
Loading…
Reference in New Issue
Block a user