diff --git a/packages/babel-plugin-resolve-type/README.md b/packages/babel-plugin-resolve-type/README.md new file mode 100644 index 00000000..0b993cbd --- /dev/null +++ b/packages/babel-plugin-resolve-type/README.md @@ -0,0 +1 @@ +# babel-plugin-resolve-type diff --git a/packages/babel-plugin-resolve-type/package.json b/packages/babel-plugin-resolve-type/package.json new file mode 100644 index 00000000..a1c050ee --- /dev/null +++ b/packages/babel-plugin-resolve-type/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vue/babel-plugin-resolve-type", + "version": "0.0.0", + "description": "Babel plugin for resolving Vue types", + "author": "三咲智子 ", + "homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-resolve-type#readme", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/babel-plugin-jsx" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch" + }, + "bugs": { + "url": "https://github.com/vuejs/babel-plugin-jsx/issues" + }, + "files": [ + "dist" + ], + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "dependencies": { + "@babel/code-frame": "^7.22.10", + "@babel/helper-module-imports": "^7.22.5", + "@babel/parser": "^7.22.11", + "@babel/plugin-syntax-typescript": "^7.22.5", + "@vue/compiler-sfc": "link:/Users/kevin/Developer/open-source/vue/vue-core/packages/compiler-sfc" + }, + "devDependencies": { + "@babel/core": "^7.22.9", + "@types/babel__code-frame": "^7.0.3", + "@types/babel__helper-module-imports": "^7.18.0", + "vue": "^3.3.4" + } +} diff --git a/packages/babel-plugin-resolve-type/src/index.ts b/packages/babel-plugin-resolve-type/src/index.ts new file mode 100644 index 00000000..928291ac --- /dev/null +++ b/packages/babel-plugin-resolve-type/src/index.ts @@ -0,0 +1,161 @@ +import type * as BabelCore from '@babel/core'; +import { parseExpression } from '@babel/parser'; +// @ts-expect-error no dts +import typescript from '@babel/plugin-syntax-typescript'; +import { + type SFCScriptCompileOptions, + type SimpleTypeResolveContext, + extractRuntimeEmits, + extractRuntimeProps, +} from '@vue/compiler-sfc'; +import { codeFrameColumns } from '@babel/code-frame'; +import { addNamed } from '@babel/helper-module-imports'; + +export interface Options { + compileOptions?: SFCScriptCompileOptions; +} + +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' && 'typeAnnotation' in props.left) { + 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, + pre(file) { + const filename = file.opts.filename || 'unknown.js'; + helpers = new Set(); + ctx = { + filename: filename, + source: file.code, + options: this.compileOptions || {}, + ast: file.ast.program.body as any, + error(msg, node) { + throw new Error( + `[@vue/babel-plugin-resolve-type] ${msg}\n\n${filename}\n${codeFrameColumns( + file.code, + { + start: { + line: node.loc!.start.line, + column: node.loc!.start.column + 1, + }, + end: { + line: node.loc!.end.line, + column: node.loc!.end.column + 1, + }, + } + )}` + ); + }, + helper(key) { + helpers!.add(key); + return `_${key}`; + }, + getString(node) { + return file.code.slice(node.start!, node.end!); + }, + bindingMetadata: Object.create(null), + propsTypeDecl: undefined, + propsRuntimeDefaults: undefined, + propsDestructuredBindings: {}, + emitsTypeDecl: undefined, + }; + }, + visitor: { + CallExpression(path) { + if (!ctx) { + throw new Error( + '[@vue/babel-plugin-resolve-type] context is not loaded.' + ); + } + + const node = path.node; + if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return; + + const comp = node.arguments[0]; + if (!comp || !t.isFunction(comp)) return; + + let options = node.arguments[1]; + if (!options) { + options = t.objectExpression([]); + 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); + }, + }, + post(file) { + for (const helper of helpers!) { + addNamed(file.path, `_${helper}`, 'vue'); + } + }, + }; +}; 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 new file mode 100644 index 00000000..baa55e9c --- /dev/null +++ b/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap @@ -0,0 +1,82 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`resolve type > runtime emits > basic 1`] = ` +"import { type SetupContext, defineComponent } from 'vue'; +const Comp = defineComponent((props, { + emit +}: SetupContext<{ + change(val: string): void; + click(): void; +}>) => { + emit('change'); + return () => {}; +}, { + emits: [\\"change\\", \\"click\\"] +});" +`; + +exports[`resolve type > runtime props > basic 1`] = ` +"import { defineComponent, h } from 'vue'; +interface Props { + msg: string; + optional?: boolean; +} +interface Props2 { + set: Set; +} +defineComponent((props: Props & Props2) => { + return () => h('div', props.msg); +}, { + props: { + msg: { + type: String, + required: true + }, + optional: { + type: Boolean, + required: false + }, + set: { + type: Set, + required: true + } + } +});" +`; + +exports[`resolve type > runtime props > with dynamic default value 1`] = ` +"import { _mergeDefaults } from \\"vue\\"; +import { defineComponent, h } from 'vue'; +const defaults = {}; +defineComponent((props: { + msg?: string; +} = defaults) => { + return () => h('div', props.msg); +}, { + props: _mergeDefaults({ + msg: { + type: String, + required: false + } + }, defaults) +});" +`; + +exports[`resolve type > runtime props > with static default value 1`] = ` +"import { defineComponent, h } from 'vue'; +defineComponent((props: { + msg?: string; +} = { + msg: 'hello' +}) => { + return () => h('div', props.msg); +}, { + props: { + msg: { + type: String, + required: false, + default: 'hello' + } + } +});" +`; diff --git a/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx new file mode 100644 index 00000000..db22e8b2 --- /dev/null +++ b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx @@ -0,0 +1,75 @@ +import { transformAsync } from '@babel/core'; +import ResolveType from '../src'; + +async function transform(code: string): Promise { + const result = await transformAsync(code, { plugins: [ResolveType] }); + return result!.code!; +} + +describe('resolve type', () => { + describe('runtime props', () => { + test('basic', async () => { + const result = await transform( + ` + import { defineComponent, h } from 'vue'; + interface Props { + msg: string; + optional?: boolean; + } + interface Props2 { + set: Set; + } + defineComponent((props: Props & Props2) => { + return () => h('div', props.msg); + }) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('with static default value', async () => { + const result = await transform( + ` + import { defineComponent, h } from 'vue'; + defineComponent((props: { msg?: string } = { msg: 'hello' }) => { + return () => h('div', props.msg); + }) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('with dynamic default value', async () => { + const result = await transform( + ` + import { defineComponent, h } from 'vue'; + const defaults = {} + defineComponent((props: { msg?: string } = defaults) => { + return () => h('div', props.msg); + }) + ` + ); + expect(result).toMatchSnapshot(); + }); + }); + + describe('runtime emits', () => { + test('basic', async () => { + const result = await transform( + ` + import { type SetupContext, defineComponent } from 'vue'; + const Comp = defineComponent( + ( + props, + { emit }: SetupContext<{ change(val: string): void; click(): void }> + ) => { + emit('change'); + return () => {}; + } + ); + ` + ); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/babel-plugin-resolve-type/tsup.config.ts b/packages/babel-plugin-resolve-type/tsup.config.ts new file mode 100644 index 00000000..7e54eecb --- /dev/null +++ b/packages/babel-plugin-resolve-type/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + target: 'node14', + platform: 'neutral', +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc9f553d..3a9b410a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,37 @@ importers: specifier: ^3.3.4 version: 3.3.4 + packages/babel-plugin-resolve-type: + dependencies: + '@babel/code-frame': + specifier: ^7.22.10 + version: 7.22.13 + '@babel/helper-module-imports': + specifier: ^7.22.5 + version: 7.22.15 + '@babel/parser': + specifier: ^7.22.11 + version: 7.22.16 + '@babel/plugin-syntax-typescript': + specifier: ^7.22.5 + version: 7.22.5(@babel/core@7.22.9) + '@vue/compiler-sfc': + specifier: link:/Users/kevin/Developer/open-source/vue/vue-core/packages/compiler-sfc + version: link:../../../vue-core/packages/compiler-sfc + devDependencies: + '@babel/core': + specifier: ^7.22.9 + version: 7.22.9 + '@types/babel__code-frame': + specifier: ^7.0.3 + version: 7.0.3 + '@types/babel__helper-module-imports': + specifier: ^7.18.0 + version: 7.18.0 + vue: + specifier: ^3.3.4 + version: 3.3.4 + packages/jsx-explorer: dependencies: '@babel/core': @@ -185,6 +216,28 @@ packages: transitivePeerDependencies: - supports-color + /@babel/core@7.22.9: + resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.9) + '@babel/helpers': 7.22.6 + '@babel/parser': 7.22.16 + '@babel/template': 7.22.15 + '@babel/traverse': 7.22.20 + '@babel/types': 7.22.19 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + /@babel/generator@7.22.15: resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==} engines: {node: '>=6.9.0'} @@ -194,6 +247,15 @@ packages: '@jridgewell/trace-mapping': 0.3.19 jsesc: 2.5.2 + /@babel/generator@7.22.9: + resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.19 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + /@babel/helper-annotate-as-pure@7.22.5: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} @@ -218,6 +280,19 @@ packages: lru-cache: 5.1.1 semver: 6.3.1 + /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.22.9 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.21.10 + lru-cache: 5.1.1 + semver: 6.3.1 + /@babel/helper-create-class-features-plugin@7.22.11(@babel/core@7.22.20): resolution: {integrity: sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==} engines: {node: '>=6.9.0'} @@ -270,7 +345,6 @@ packages: /@babel/helper-environment-visitor@7.22.5: resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-function-name@7.22.5: resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} @@ -325,6 +399,19 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -414,6 +501,16 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helpers@7.22.6: + resolution: {integrity: sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.22.20 + '@babel/types': 7.22.19 + transitivePeerDependencies: + - supports-color + /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} @@ -635,6 +732,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: false + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.22.20): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} @@ -1711,6 +1818,20 @@ packages: engines: {node: '>= 10'} dev: true + /@types/babel__code-frame@7.0.3: + resolution: {integrity: sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==} + dev: true + + /@types/babel__core@7.20.1: + resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==} + dependencies: + '@babel/parser': 7.22.16 + '@babel/types': 7.22.19 + '@types/babel__generator': 7.6.4 + '@types/babel__template': 7.4.2 + '@types/babel__traverse': 7.20.2 + dev: true + /@types/babel__core@7.20.2: resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==} dependencies: @@ -1727,6 +1848,13 @@ packages: '@babel/types': 7.22.19 dev: true + /@types/babel__helper-module-imports@7.18.0: + resolution: {integrity: sha512-bXrjmO0EhInafHtFIaimws2rDf8Sp0E6T3cstzSL4lUAPtzZ2GhoV48U6n4IHyBIBsd88r4JIw2UPTqlyWwXcg==} + dependencies: + '@types/babel__core': 7.20.1 + '@types/babel__traverse': 7.20.2 + dev: true + /@types/babel__template@7.4.2: resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} dependencies: