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