mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-04-24 10:32:33 +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' }],
|
'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
1
global.d.ts
vendored
@ -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';
|
||||||
|
@ -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) }],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
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 {
|
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;
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
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;
|
||||||
|
|
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": {
|
"compilerOptions": {
|
||||||
"rootDirs": ["./src"],
|
"rootDirs": ["./src"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"downlevelIteration": true
|
"downlevelIteration": true,
|
||||||
|
"types": ["node", "jest"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user