From 516685b16ac3316e2afb10c2e1035b20382199af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Tue, 26 Sep 2023 01:01:09 +0800 Subject: [PATCH] feat: infer component name --- .../babel-plugin-resolve-type/src/index.ts | 200 ++++++++++++------ .../__snapshots__/resolve-type.test.tsx.snap | 72 ++++++- .../test/resolve-type.test.tsx | 83 +++++++- 3 files changed, 283 insertions(+), 72 deletions(-) diff --git a/packages/babel-plugin-resolve-type/src/index.ts b/packages/babel-plugin-resolve-type/src/index.ts index 1e4696bf..aed87092 100644 --- a/packages/babel-plugin-resolve-type/src/index.ts +++ b/packages/babel-plugin-resolve-type/src/index.ts @@ -11,71 +11,12 @@ import { import { codeFrameColumns } from '@babel/code-frame'; import { addNamed } from '@babel/helper-module-imports'; -function getTypeAnnotation(node: BabelCore.types.Node) { - if ( - 'typeAnnotation' in node && - node.typeAnnotation && - node.typeAnnotation.type === 'TSTypeAnnotation' - ) { - return node.typeAnnotation.typeAnnotation; - } -} - export default ({ types: t, }: typeof BabelCore): BabelCore.PluginObj => { let ctx: SimpleTypeResolveContext | undefined; let helpers: Set | undefined; - function processProps( - comp: BabelCore.types.Function, - options: BabelCore.types.ObjectExpression - ) { - const props = comp.params[0]; - if (!props) return; - - if (props.type === 'AssignmentPattern') { - ctx!.propsTypeDecl = getTypeAnnotation(props.left); - ctx!.propsRuntimeDefaults = props.right; - } else { - ctx!.propsTypeDecl = getTypeAnnotation(props); - } - - if (!ctx!.propsTypeDecl) return; - - const runtimeProps = extractRuntimeProps(ctx!); - if (!runtimeProps) { - return; - } - - const ast = parseExpression(runtimeProps); - options.properties.push(t.objectProperty(t.identifier('props'), ast)); - } - - function processEmits( - comp: BabelCore.types.Function, - options: BabelCore.types.ObjectExpression - ) { - const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]); - if ( - !setupCtx || - !t.isTSTypeReference(setupCtx) || - !t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) - ) - return; - - const emitType = setupCtx.typeParameters?.params[0]; - if (!emitType) return; - - ctx!.emitsTypeDecl = emitType; - const runtimeEmits = extractRuntimeEmits(ctx!); - - const ast = t.arrayExpression( - Array.from(runtimeEmits).map((e) => t.stringLiteral(e)) - ); - options.properties.push(t.objectProperty(t.identifier('emits'), ast)); - } - return { name: 'babel-plugin-resolve-type', inherits: typescript, @@ -125,8 +66,10 @@ export default ({ ); } - const node = path.node; + const { node } = path; + if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return; + if (!checkDefineComponent(path)) return; const comp = node.arguments[0]; if (!comp || !t.isFunction(comp)) return; @@ -137,14 +80,11 @@ export default ({ node.arguments.push(options); } - if (!t.isObjectExpression(options)) { - throw new Error( - '[@vue/babel-plugin-resolve-type] Options inside of defineComponent should be an object expression.' - ); - } - - processProps(comp, options); - processEmits(comp, options); + node.arguments[1] = processProps(comp, options) || options; + node.arguments[1] = processEmits(comp, node.arguments[1]) || options; + }, + VariableDeclarator(path) { + inferComponentName(path); }, }, post(file) { @@ -153,4 +93,128 @@ export default ({ } }, }; + + function inferComponentName( + path: BabelCore.NodePath + ) { + const id = path.get('id'); + const init = path.get('init'); + if (!id || !id.isIdentifier() || !init || !init.isCallExpression()) return; + + if (!init.get('callee')?.isIdentifier({ name: 'defineComponent' })) return; + if (!checkDefineComponent(init)) return; + + const nameProperty = t.objectProperty( + t.identifier('name'), + t.stringLiteral(id.node.name) + ); + const { arguments: args } = init.node; + if (args.length === 0) return; + + if (args.length === 1) { + init.node.arguments.push(t.objectExpression([])); + } + args[1] = addProperty(t, args[1], nameProperty); + } + + function processProps( + comp: BabelCore.types.Function, + options: + | BabelCore.types.ArgumentPlaceholder + | BabelCore.types.JSXNamespacedName + | BabelCore.types.SpreadElement + | BabelCore.types.Expression + ) { + const props = comp.params[0]; + if (!props) return; + + if (props.type === 'AssignmentPattern') { + ctx!.propsTypeDecl = getTypeAnnotation(props.left); + ctx!.propsRuntimeDefaults = props.right; + } else { + ctx!.propsTypeDecl = getTypeAnnotation(props); + } + + if (!ctx!.propsTypeDecl) return; + + const runtimeProps = extractRuntimeProps(ctx!); + if (!runtimeProps) { + return; + } + + const ast = parseExpression(runtimeProps); + return addProperty( + t, + options, + t.objectProperty(t.identifier('props'), ast) + ); + } + + function processEmits( + comp: BabelCore.types.Function, + options: + | BabelCore.types.ArgumentPlaceholder + | BabelCore.types.JSXNamespacedName + | BabelCore.types.SpreadElement + | BabelCore.types.Expression + ) { + const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]); + if ( + !setupCtx || + !t.isTSTypeReference(setupCtx) || + !t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) + ) + return; + + const emitType = setupCtx.typeParameters?.params[0]; + if (!emitType) return; + + ctx!.emitsTypeDecl = emitType; + const runtimeEmits = extractRuntimeEmits(ctx!); + + const ast = t.arrayExpression( + Array.from(runtimeEmits).map((e) => t.stringLiteral(e)) + ); + return addProperty( + t, + options, + t.objectProperty(t.identifier('emits'), ast) + ); + } }; + +function getTypeAnnotation(node: BabelCore.types.Node) { + if ( + 'typeAnnotation' in node && + node.typeAnnotation && + node.typeAnnotation.type === 'TSTypeAnnotation' + ) { + return node.typeAnnotation.typeAnnotation; + } +} + +function checkDefineComponent( + path: BabelCore.NodePath +) { + const defineCompImport = + path.scope.getBinding('defineComponent')?.path.parent; + if (!defineCompImport) return true; + + return ( + defineCompImport.type === 'ImportDeclaration' && + /^@?vue(\/|$)/.test(defineCompImport.source.value) + ); +} + +function addProperty( + t: (typeof BabelCore)['types'], + object: T, + property: BabelCore.types.ObjectProperty +) { + if (t.isObjectExpression(object)) { + object.properties.unshift(property); + } else if (t.isExpression(object)) { + return t.objectExpression([property, t.spreadElement(object)]); + } + return object; +} diff --git a/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap b/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap index ccf0e45a..b6f3b45d 100644 --- a/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap +++ b/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap @@ -1,8 +1,76 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`resolve type > defineComponent scope > fake 1`] = ` +"const defineComponent = () => {}; +defineComponent((props: { + msg?: string; +}) => { + return () =>
; +});" +`; + +exports[`resolve type > defineComponent scope > import sub-package 1`] = ` +"import { defineComponent } from 'vue/dist/vue.esm-bundler'; +defineComponent((props: { + msg?: string; +}) => { + return () =>
; +}, { + props: { + msg: { + type: String, + required: false + } + } +});" +`; + +exports[`resolve type > defineComponent scope > w/o import 1`] = ` +"defineComponent((props: { + msg?: string; +}) => { + return () =>
; +}, { + props: { + msg: { + type: String, + required: false + } + } +});" +`; + +exports[`resolve type > infer component name > identifier options 1`] = ` +"import { defineComponent } from 'vue'; +const Foo = defineComponent(() => {}, { + name: \\"Foo\\", + ...opts +});" +`; + +exports[`resolve type > infer component name > no options 1`] = ` +"import { defineComponent } from 'vue'; +const Foo = defineComponent(() => {}, { + name: \\"Foo\\" +});" +`; + +exports[`resolve type > infer component name > object options 1`] = ` +"import { defineComponent } from 'vue'; +const Foo = defineComponent(() => {}, { + name: \\"Foo\\", + foo: 'bar' +});" +`; + +exports[`resolve type > infer component name > rest param 1`] = ` +"import { defineComponent } from 'vue'; +const Foo = defineComponent(() => {}, ...args);" +`; + exports[`resolve type > runtime emits > basic 1`] = ` "import { type SetupContext, defineComponent } from 'vue'; -const Comp = defineComponent((props, { +defineComponent((props, { emit }: SetupContext<{ change(val: string): void; @@ -83,7 +151,7 @@ defineComponent((props: { exports[`resolve type > w/ tsx 1`] = ` "import { type SetupContext, defineComponent } from 'vue'; -const Comp = defineComponent(() => { +defineComponent(() => { return () =>
; }, {});" `; diff --git a/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx index 5830de43..deccd90f 100644 --- a/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx +++ b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx @@ -60,7 +60,7 @@ describe('resolve type', () => { const result = await transform( ` import { type SetupContext, defineComponent } from 'vue'; - const Comp = defineComponent( + defineComponent( ( props, { emit }: SetupContext<{ change(val: string): void; click(): void }> @@ -79,11 +79,90 @@ describe('resolve type', () => { const result = await transform( ` import { type SetupContext, defineComponent } from 'vue'; - const Comp = defineComponent(() => { + defineComponent(() => { return () =>
; }); ` ); expect(result).toMatchSnapshot(); }); + + describe('defineComponent scope', () => { + test('fake', async () => { + const result = await transform( + ` + const defineComponent = () => {}; + defineComponent((props: { msg?: string }) => { + return () =>
; + }); + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('w/o import', async () => { + const result = await transform( + ` + defineComponent((props: { msg?: string }) => { + return () =>
; + }); + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('import sub-package', async () => { + const result = await transform( + ` + import { defineComponent } from 'vue/dist/vue.esm-bundler'; + defineComponent((props: { msg?: string }) => { + return () =>
; + }); + ` + ); + expect(result).toMatchSnapshot(); + }); + }); + + describe('infer component name', () => { + test('no options', async () => { + const result = await transform( + ` + import { defineComponent } from 'vue'; + const Foo = defineComponent(() => {}) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('object options', async () => { + const result = await transform( + ` + import { defineComponent } from 'vue'; + const Foo = defineComponent(() => {}, { foo: 'bar' }) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('identifier options', async () => { + const result = await transform( + ` + import { defineComponent } from 'vue'; + const Foo = defineComponent(() => {}, opts) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('rest param', async () => { + const result = await transform( + ` + import { defineComponent } from 'vue'; + const Foo = defineComponent(() => {}, ...args) + ` + ); + expect(result).toMatchSnapshot(); + }); + }); });