From ebbd992ba08300d24aae29704eb651fab73ecaa6 Mon Sep 17 00:00:00 2001
From: Amour1688 <31695475+Amour1688@users.noreply.github.com>
Date: Sat, 25 Jul 2020 22:39:19 +0800
Subject: [PATCH] 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
---
.eslintrc.js | 3 +-
global.d.ts | 1 +
packages/babel-plugin-jsx/babel.config.js | 10 +-
packages/babel-plugin-jsx/example/index.js | 77 ++--
packages/babel-plugin-jsx/jest.config.js | 10 +-
packages/babel-plugin-jsx/package.json | 3 +
packages/babel-plugin-jsx/src/buildProps.ts | 353 ++++++++++++++++++
packages/babel-plugin-jsx/src/index.ts | 3 +-
.../babel-plugin-jsx/src/parseDirectives.ts | 39 +-
.../babel-plugin-jsx/src/transform-vue-jsx.ts | 350 +----------------
packages/babel-plugin-jsx/src/utils.ts | 6 +-
.../babel-plugin-jsx/test/coverage.test.js | 13 -
.../babel-plugin-jsx/test/coverage.test.ts | 16 +
.../test/{index.test.js => index.test.tsx} | 158 +++++---
.../test/{setup.js => setup.ts} | 0
.../babel-plugin-jsx/test/v-model.test.tsx | 166 ++++++++
packages/babel-plugin-jsx/tsconfig.json | 3 +-
tsconfig.json | 2 +-
18 files changed, 722 insertions(+), 491 deletions(-)
create mode 100644 packages/babel-plugin-jsx/src/buildProps.ts
delete mode 100644 packages/babel-plugin-jsx/test/coverage.test.js
create mode 100644 packages/babel-plugin-jsx/test/coverage.test.ts
rename packages/babel-plugin-jsx/test/{index.test.js => index.test.tsx} (76%)
rename packages/babel-plugin-jsx/test/{setup.js => setup.ts} (100%)
create mode 100644 packages/babel-plugin-jsx/test/v-model.test.tsx
diff --git a/.eslintrc.js b/.eslintrc.js
index b1d2a0b..133bb18 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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': {
diff --git a/global.d.ts b/global.d.ts
index 3b3a1f2..4629732 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -1,2 +1,3 @@
+declare module '*.js';
declare module '@babel/helper-module-imports';
declare module '@babel/plugin-syntax-jsx';
diff --git a/packages/babel-plugin-jsx/babel.config.js b/packages/babel-plugin-jsx/babel.config.js
index 4c835be..6dface2 100644
--- a/packages/babel-plugin-jsx/babel.config.js
+++ b/packages/babel-plugin-jsx/babel.config.js
@@ -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) }],
],
};
diff --git a/packages/babel-plugin-jsx/example/index.js b/packages/babel-plugin-jsx/example/index.js
index c7030c8..d0b0eb8 100644
--- a/packages/babel-plugin-jsx/example/index.js
+++ b/packages/babel-plugin-jsx/example/index.js
@@ -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 (
-
- Super
-
-
- );
-};
-
-SuperButton.inheritAttrs = false;
-
-const App = defineComponent(() => {
- const count = ref(0);
- const inc = () => {
- count.value++;
- };
-
- const obj = {
- click: inc,
- mouseover: inc,
- };
-
- return () => (
-
- Foo {count.value}
-
-
-
-
- );
+const Child = defineComponent({
+ props: ['foo'],
+ setup(props) {
+ return () => {props.foo}
;
+ },
});
-createApp(App).mount('#app');
+Child.inheritAttrs = false;
+
+const App = defineComponent({
+ data: () => ({
+ test: '1',
+ }),
+ render() {
+ return (
+ <>
+
+
+ >
+ );
+ },
+});
+
+const app = createApp(App);
+
+app.mount('#app');
+
+console.log(app);
diff --git a/packages/babel-plugin-jsx/jest.config.js b/packages/babel-plugin-jsx/jest.config.js
index ca85779..ed4ad12 100644
--- a/packages/babel-plugin-jsx/jest.config.js
+++ b/packages/babel-plugin-jsx/jest.config.js
@@ -1,3 +1,11 @@
module.exports = {
- setupFiles: ['./test/setup.js'],
+ setupFiles: ['./test/setup.ts'],
+ transform: {
+ '\\.(ts|tsx)$': 'ts-jest',
+ },
+ globals: {
+ 'ts-jest': {
+ babelConfig: true,
+ },
+ },
};
diff --git a/packages/babel-plugin-jsx/package.json b/packages/babel-plugin-jsx/package.json
index 3a73eee..3bf02eb 100644
--- a/packages/babel-plugin-jsx/package.json
+++ b/packages/babel-plugin-jsx/package.json
@@ -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",
diff --git a/packages/babel-plugin-jsx/src/buildProps.ts b/packages/babel-plugin-jsx/src/buildProps.ts
new file mode 100644
index 0000000..318916e
--- /dev/null
+++ b/packages/babel-plugin-jsx/src/buildProps.ts
@@ -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,
+ 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,
+ mergeArgs: (t.ObjectProperty | t.Expression)[],
+) => {
+ const argument = path.get('argument') as NodePath;
+ 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();
+ 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, 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,
+ 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;
diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts
index a7c1157..3e7b6a5 100644
--- a/packages/babel-plugin-jsx/src/index.ts
+++ b/packages/babel-plugin-jsx/src/index.ts
@@ -11,7 +11,8 @@ export type State = {
interface Opts {
transformOn?: boolean;
compatibleProps?: boolean;
- usePatchFlag?: boolean;
+ optimize?: boolean;
+ isCustomElement?: (tag: string) => boolean;
}
export type ExcludesBoolean = (x: T | false | true) => x is T;
diff --git a/packages/babel-plugin-jsx/src/parseDirectives.ts b/packages/babel-plugin-jsx/src/parseDirectives.ts
index 76df772..004412f 100644
--- a/packages/babel-plugin-jsx/src/parseDirectives.ts
+++ b/packages/babel-plugin-jsx/src/parseDirectives.ts
@@ -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) => {
return false;
}
return t.isJSXIdentifier(attribute.get('name'))
- && (attribute.get('name') as NodePath).get('name') === 'type'
- && t.isStringLiteral(attribute.get('value'));
- });
+ && (attribute.get('name') as NodePath).node.name === 'type';
+ }) as NodePath | 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;
diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts
index 4d2f915..c1b079c 100644
--- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts
+++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts
@@ -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,
- mergeArgs: (t.ObjectProperty | t.Expression)[],
-) => {
- const argument = path.get('argument') as NodePath;
- 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,
- 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();
- 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, 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,
- 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
@@ -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, state: State) {
diff --git a/packages/babel-plugin-jsx/src/utils.ts b/packages/babel-plugin-jsx/src/utils.ts
index 0f01234..cd85d10 100644
--- a/packages/babel-plugin-jsx/src/utils.ts
+++ b/packages/babel-plugin-jsx/src/utils.ts
@@ -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);
diff --git a/packages/babel-plugin-jsx/test/coverage.test.js b/packages/babel-plugin-jsx/test/coverage.test.js
deleted file mode 100644
index 3554791..0000000
--- a/packages/babel-plugin-jsx/test/coverage.test.js
+++ /dev/null
@@ -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',
- });
-});
diff --git a/packages/babel-plugin-jsx/test/coverage.test.ts b/packages/babel-plugin-jsx/test/coverage.test.ts
new file mode 100644
index 0000000..7abfd0c
--- /dev/null
+++ b/packages/babel-plugin-jsx/test/coverage.test.ts
@@ -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,
+ });
+ });
+});
diff --git a/packages/babel-plugin-jsx/test/index.test.js b/packages/babel-plugin-jsx/test/index.test.tsx
similarity index 76%
rename from packages/babel-plugin-jsx/test/index.test.js
rename to packages/babel-plugin-jsx/test/index.test.tsx
index 3c7cb3a..e71b3d8 100644
--- a/packages/babel-plugin-jsx/test/index.test.js
+++ b/packages/babel-plugin-jsx/test/index.test.tsx
@@ -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,
+ 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 () => ;
+ return () => ;
},
});
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) => {props.foo}
;
+ const Child = defineComponent({
+ props: {
+ foo: Number,
+ },
+ setup(props) {
+ return () => {props.foo}
;
+ },
+ });
Child.inheritAttrs = false;
const wrapper = mount({
- setup() {
- return () => (
+ render() {
+ return (
);
},
@@ -83,9 +95,15 @@ describe('Transform JSX', () => {
});
test('nested component', () => {
- const A = {};
+ const A = {
+ B: defineComponent({
+ setup() {
+ return () => 123
;
+ },
+ }),
+ };
- A.B = () => 123
;
+ A.B.inheritAttrs = false;
const wrapper = mount(() => );
@@ -104,6 +122,7 @@ describe('Transform JSX', () => {
test('Merge class', () => {
const wrapper = shallowMount({
setup() {
+ // @ts-ignore
return () => ;
},
});
@@ -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 () => ;
},
});
@@ -157,52 +172,51 @@ describe('Transform JSX', () => {
});
test('domProps input[checked]', () => {
- const val = 'foo';
+ const val = true;
const wrapper = shallowMount({
setup() {
return () => ;
},
});
- 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 ;
},
});
- 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 ;
},
});
- 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 ;
+ return 123
;
},
});
- expect(wrapper.html()).toBe('123
');
+ expect(wrapper.html()).toBe('123
');
});
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 () => (
- {
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 () =>
;
+ },
+ }));
+ expect(wrapper.html()).toBe('
foo
');
+ });
+
+ test('vText', () => {
+ const text = 'foo';
+ const wrapper = shallowMount(({
+ setup() {
+ return () =>
;
+ },
+ }));
+ expect(wrapper.html()).toBe('
foo
');
+ });
});
describe('slots', () => {
test('with default', () => {
- const A = (_, { slots }) => (
-
- {slots.default()}
- {slots.foo('val')}
-
- );
+ const A = defineComponent({
+ setup(_, { slots }) {
+ return () => (
+
+ {slots.default?.()}
+ {slots.foo?.('val')}
+
+ );
+ },
+ });
A.inheritAttrs = false;
const wrapper = mount({
setup() {
- const slots = {
- foo: (val) => val,
- };
- return () =>
default;
+ return () =>
val }}>default;
},
});
@@ -287,20 +323,21 @@ describe('slots', () => {
});
test('without default', () => {
- const A = (_, { slots }) => (
-
- {slots.foo('foo')}
-
- );
+ const A = defineComponent({
+ setup(_, { slots }) {
+ return () => (
+
+ {slots.foo?.('foo')}
+
+ );
+ },
+ });
A.inheritAttrs = false;
const wrapper = mount({
setup() {
- const slots = {
- foo: (val) => val,
- };
- return () =>
;
+ return () =>
val }} />;
},
});
@@ -325,7 +362,7 @@ describe('PatchFlags', () => {
const onClick = () => {
visible.value = false;
};
- return () => NEED_PATCH
;
+ return () => NEED_PATCH
;
},
});
@@ -355,12 +392,15 @@ describe('PatchFlags', () => {
});
});
-describe('variables outside slots', async () => {
- const A = {
+describe('variables outside slots', () => {
+ interface AProps {
+ inc: () => void
+ }
+ const A = defineComponent({
render() {
- return this.$slots.default();
+ return this.$slots.default?.();
},
- };
+ });
A.inheritAttrs = false;
diff --git a/packages/babel-plugin-jsx/test/setup.js b/packages/babel-plugin-jsx/test/setup.ts
similarity index 100%
rename from packages/babel-plugin-jsx/test/setup.js
rename to packages/babel-plugin-jsx/test/setup.ts
diff --git a/packages/babel-plugin-jsx/test/v-model.test.tsx b/packages/babel-plugin-jsx/test/v-model.test.tsx
new file mode 100644
index 0000000..71d0a8b
--- /dev/null
+++ b/packages/babel-plugin-jsx/test/v-model.test.tsx
@@ -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 ;
+ },
+ });
+
+ 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 (
+ <>
+
+
+ >
+ );
+ },
+ });
+
+ 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 (
+
+ );
+ },
+ });
+
+ 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 ;
+ },
+ });
+ 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 ;
+ },
+ });
+ 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 ;
+ },
+ });
+ 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 ;
+ },
+ });
+
+ expect(wrapper.vm.$el.checked).toBe(true);
+ wrapper.vm.test = false;
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.$el.checked).toBe(false);
+});
diff --git a/packages/babel-plugin-jsx/tsconfig.json b/packages/babel-plugin-jsx/tsconfig.json
index 6169214..608bfba 100644
--- a/packages/babel-plugin-jsx/tsconfig.json
+++ b/packages/babel-plugin-jsx/tsconfig.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"rootDirs": ["./src"],
"outDir": "dist",
- "downlevelIteration": true
+ "downlevelIteration": true,
+ "types": ["node", "jest"]
},
"include": [
"src/**/*",
diff --git a/tsconfig.json b/tsconfig.json
index 5d76b10..2598776 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
- "allowJs": false,
+ "allowJs": true,
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,