From 672f27d258beb96d07bdd4f9535a6f9500eb7bd7 Mon Sep 17 00:00:00 2001 From: Amour1688 Date: Mon, 13 Jul 2020 23:44:52 +0800 Subject: [PATCH] refactor: new directive API --- README-zh_CN.md | 30 +++- README.md | 32 ++++- .../babel-plugin-jsx/src/parseDirectives.ts | 131 ++++++++++++++++++ .../babel-plugin-jsx/src/transform-vue-jsx.ts | 24 ++-- packages/babel-plugin-jsx/src/utils.ts | 108 +-------------- 5 files changed, 197 insertions(+), 128 deletions(-) create mode 100644 packages/babel-plugin-jsx/src/parseDirectives.ts diff --git a/README-zh_CN.md b/README-zh_CN.md index cca7800..9d9dd0b 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -137,6 +137,28 @@ export default { } ``` +* 或者使用这种实现 + +```jsx + +``` + +```jsx + +``` + +会变编译成: + +```js +h(A, { + 'foo': val, + "fooModifiers": { + "bar": true + }, + "onUpdate:foo": $event => val = $event +}) +``` + 自定义指令 ```jsx @@ -145,17 +167,15 @@ const App = { setup() { return () => ( ); }, } ``` +> 注意:如果想要使用 `arg`, 第二个参数需要为字符串 + ### 插槽 ```jsx diff --git a/README.md b/README.md index a95bc52..b453b47 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ const App = { v-model -* You should use underscore (`_`) instead of dot (`.`) for modifiers (`vModel_trim={this.test}`) +* You can use underscore (`_`) instead of dot (`.`) for modifiers (`vModel_trim={this.test}`) ```jsx export default { @@ -138,6 +138,28 @@ export default { } ``` +* Or you can use this proposal. + +```jsx + +``` + +```jsx + +``` + +Will compile to: + +```js +h(A, { + 'foo': val, + "fooModifiers": { + "bar": true + }, + "onUpdate:foo": $event => val = $event +}) +``` + custom directive ```jsx @@ -146,17 +168,15 @@ const App = { setup() { return () => ( ); }, } ``` +> Note: You should pass the second param as string for using `arg`. + ### Slot ```jsx diff --git a/packages/babel-plugin-jsx/src/parseDirectives.ts b/packages/babel-plugin-jsx/src/parseDirectives.ts new file mode 100644 index 0000000..cdc0c22 --- /dev/null +++ b/packages/babel-plugin-jsx/src/parseDirectives.ts @@ -0,0 +1,131 @@ +import * as t from '@babel/types'; +import { NodePath } from '@babel/traverse'; +import { createIdentifier } from './utils'; +import { State, ExcludesBoolean } from './'; + +/** + * Get JSX element type + * + * @param path Path + */ +const getType = (path: NodePath) => { + const typePath = path + .get('attributes') + .find((attribute) => { + if (!t.isJSXAttribute(attribute)) { + return false; + } + return t.isJSXIdentifier(attribute.get('name')) + && (attribute.get('name') as NodePath).get('name') === 'type' + && t.isStringLiteral(attribute.get('value')) + }, + ); + + return typePath ? typePath.get('value.value') : ''; +}; + +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) + } + return modifiers; +} + +const parseDirectives = (args: { + name: string, + path: NodePath, + value: t.StringLiteral | t.Expression | null, + state: State, + tag: t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression, + isComponent: boolean +}) => { + const { + name, path, value, state, tag, isComponent, + } = args + let modifiers: string[] = name.split('_'); + let arg; + let val; + + const directiveName: string = modifiers.shift() + ?.replace(/^v/, '') + .replace(/^-/, '') + .replace(/^\S/, (s: string) => s.toLowerCase()) || ''; + + if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) { + throw new Error('You have to use JSX Expression inside your v-model'); + } + + const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent); + + if (t.isArrayExpression(value)) { + const { elements } = value as t.ArrayExpression; + const [first, second, third] = elements; + if (t.isStringLiteral(second)) { + arg = second; + modifiers = parseModifiers(third as t.Expression); + } else if (second) { + modifiers = parseModifiers(second as t.Expression); + } + val = first; + } + + const modifiersSet = new Set(modifiers); + + return { + directiveName, + modifiers: modifiersSet, + value: val || value, + arg, + directive: hasDirective ? [ + resolveDirective(path, state, tag, directiveName), + val as t.Identifier, + !!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true), + !!modifiersSet.size && t.objectExpression( + [...modifiersSet].map( + (modifier) => t.objectProperty( + t.identifier(modifier as string), + t.booleanLiteral(true), + ), + ), + ), + ].filter(Boolean as any as ExcludesBoolean) : undefined, + }; +}; + +const resolveDirective = (path: NodePath, state: State, tag: any, directiveName: string) => { + if (directiveName === 'show') { + return createIdentifier(state, 'vShow'); + } + if (directiveName === 'model') { + let modelToUse; + const type = getType(path.parentPath as NodePath); + switch (tag.value) { + case 'select': + modelToUse = createIdentifier(state, 'vModelSelect'); + break; + case 'textarea': + 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'); + } + } + return modelToUse; + } + return t.callExpression( + createIdentifier(state, 'resolveDirective'), [ + t.stringLiteral(directiveName), + ], + ); +}; + +export default parseDirectives; diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 4f5a721..6e98d8f 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -10,10 +10,10 @@ import { getJSXAttributeName, transformJSXText, transformJSXExpressionContainer, - parseDirectives, isFragment, walksScope, } from './utils'; +import parseDirectives from './parseDirectives'; import { PatchFlags, PatchFlagNames } from './patchFlags'; import { State, ExcludesBoolean } from './'; @@ -198,7 +198,7 @@ const buildProps = (path: NodePath, state: State) => { return; } if (isDirective(name)) { - const { directive, modifiers, directiveName } = parseDirectives({ + const { directive, modifiers, value, arg, directiveName } = parseDirectives({ tag, isComponent, name, @@ -206,6 +206,8 @@ const buildProps = (path: NodePath, state: State) => { state, value: attributeValue, }); + const argVal = (arg as t.StringLiteral)?.value; + const propName = argVal || 'modelValue'; if (directiveName === 'slots') { slots = attributeValue; @@ -215,16 +217,16 @@ const buildProps = (path: NodePath, state: State) => { } else { // must be v-model and is a component properties.push(t.objectProperty( - t.stringLiteral('modelValue'), + arg || t.stringLiteral('modelValue'), // @ts-ignore - attributeValue, + value, )); - dynamicPropNames.add('modelValue'); + dynamicPropNames.add(propName); if (modifiers.size) { properties.push(t.objectProperty( - t.stringLiteral('modelModifiers'), + t.stringLiteral(`${argVal || 'model'}Modifiers`), t.objectExpression( [...modifiers].map((modifier) => ( t.objectProperty( @@ -237,17 +239,19 @@ const buildProps = (path: NodePath, state: State) => { } } - if (directiveName === 'model' && attributeValue) { + console.log(value) + + if (directiveName === 'model' && value) { properties.push(t.objectProperty( - t.stringLiteral('onUpdate:modelValue'), + t.stringLiteral(`onUpdate:${propName}`), t.arrowFunctionExpression( [t.identifier('$event')], // @ts-ignore - t.assignmentExpression('=', attributeValue, t.identifier('$event')), + t.assignmentExpression('=', value, t.identifier('$event')), ), )); - dynamicPropNames.add('onUpdate:modelValue'); + dynamicPropNames.add(`onUpdate:${propName}`); } return; } diff --git a/packages/babel-plugin-jsx/src/utils.ts b/packages/babel-plugin-jsx/src/utils.ts index 2fb562b..75ed603 100644 --- a/packages/babel-plugin-jsx/src/utils.ts +++ b/packages/babel-plugin-jsx/src/utils.ts @@ -2,7 +2,7 @@ import * as t from '@babel/types'; import htmlTags from 'html-tags'; import svgTags from 'svg-tags'; import { NodePath } from '@babel/traverse'; -import { State, ExcludesBoolean } from './'; +import { State } from './'; /** * create Identifier @@ -169,111 +169,6 @@ const transformJSXSpreadChild = ( path: NodePath ): t.SpreadElement => t.spreadElement(path.get('expression').node); -/** - * Get JSX element type - * - * @param path Path - */ -const getType = (path: NodePath) => { - const typePath = path - .get('attributes') - .find((attribute) => { - if (!t.isJSXAttribute(attribute)) { - return false; - } - return t.isJSXIdentifier(attribute.get('name')) - && (attribute.get('name') as NodePath).get('name') === 'type' - && t.isStringLiteral(attribute.get('value')) - }, - ); - - return typePath ? typePath.get('value.value') : ''; -}; - -const resolveDirective = (path: NodePath, state: State, tag: any, directiveName: string) => { - if (directiveName === 'show') { - return createIdentifier(state, 'vShow'); - } if (directiveName === 'model') { - let modelToUse; - const type = getType(path.parentPath as NodePath); - switch (tag.value) { - case 'select': - modelToUse = createIdentifier(state, 'vModelSelect'); - break; - case 'textarea': - 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'); - } - } - return modelToUse; - } - return t.callExpression( - createIdentifier(state, 'resolveDirective'), [ - t.stringLiteral(directiveName), - ], - ); -}; - -/** - * Parse directives metadata - * - * @param path JSXAttribute - * @returns null | Object<{ modifiers: Set, valuePath: Path}> - */ -const parseDirectives = (args: { - name: string, - path: NodePath, - value: t.StringLiteral | t.Expression | null, - state: State, - tag: t.Identifier | t.MemberExpression | t.StringLiteral | t.CallExpression, - isComponent: boolean -}) => { - const { - name, path, value, state, tag, isComponent, - } = args - const modifiers: string[] = name.split('_'); - const directiveName: string = modifiers.shift() - ?.replace(/^v/, '') - .replace(/^-/, '') - .replace(/^\S/, (s: string) => s.toLowerCase()) || ''; - - if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) { - throw new Error('You have to use JSX Expression inside your v-model'); - } - - const modifiersSet = new Set(modifiers); - - const hasDirective = directiveName !== 'model' || (directiveName === 'model' && !isComponent); - - return { - directiveName, - modifiers: modifiersSet, - directive: hasDirective ? [ - resolveDirective(path, state, tag, directiveName), - value, - !!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true), - !!modifiersSet.size && t.objectExpression( - [...modifiersSet].map( - (modifier) => t.objectProperty( - t.identifier(modifier), - t.booleanLiteral(true), - ), - ), - ), - ].filter(Boolean as any as ExcludesBoolean) : undefined, - }; -}; - const walksScope = (path: NodePath, name: string) => { if (path.scope.hasBinding(name) && path.parentPath) { path.parentPath.setData('optimize', false); @@ -291,7 +186,6 @@ export { transformJSXText, transformJSXSpreadChild, transformJSXExpressionContainer, - parseDirectives, isFragment, walksScope, };