From 6760d0b4591e9130e6d10a5540eedc0d33057683 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:53:05 +0800 Subject: [PATCH] feat: integrate with JSX plugin --- package.json | 1 + packages/babel-plugin-jsx/package.json | 3 + packages/babel-plugin-jsx/src/index.ts | 341 +++++++++--------- packages/babel-plugin-jsx/src/interface.ts | 5 + .../__snapshots__/resolve-type.test.tsx.snap | 17 + .../test/__snapshots__/snapshot.test.ts.snap | 2 + .../test/resolve-type.test.tsx | 21 ++ .../babel-plugin-resolve-type/package.json | 3 +- .../babel-plugin-resolve-type/src/index.ts | 11 +- pnpm-lock.yaml | 27 +- 10 files changed, 259 insertions(+), 172 deletions(-) create mode 100644 packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap create mode 100644 packages/babel-plugin-jsx/test/resolve-type.test.tsx diff --git a/package.json b/package.json index 3c937341..bbf36522 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "jsx" ], "devDependencies": { + "@babel/plugin-syntax-typescript": "^7.22.5", "@rollup/plugin-babel": "^6.0.3", "@types/babel__core": "^7.20.2", "@types/node": "^20.6.5", diff --git a/packages/babel-plugin-jsx/package.json b/packages/babel-plugin-jsx/package.json index 344801a8..87e07c76 100644 --- a/packages/babel-plugin-jsx/package.json +++ b/packages/babel-plugin-jsx/package.json @@ -24,11 +24,13 @@ ], "dependencies": { "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5", "@babel/template": "^7.22.15", "@babel/traverse": "^7.22.20", "@babel/types": "^7.22.19", "@vue/babel-helper-vue-transform-on": "workspace:^", + "@vue/babel-plugin-resolve-type": "workspace:^", "camelcase": "^6.3.0", "html-tags": "^3.3.1", "svg-tags": "^1.0.0" @@ -36,6 +38,7 @@ "devDependencies": { "@babel/core": "^7.22.20", "@babel/preset-env": "^7.22.20", + "@types/babel__helper-plugin-utils": "^7.10.1", "@types/babel__template": "^7.4.2", "@types/babel__traverse": "^7.20.2", "@types/svg-tags": "^1.0.0", diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index 18214793..ecb69dc4 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -5,7 +5,9 @@ import template from '@babel/template'; import syntaxJsx from '@babel/plugin-syntax-jsx'; // @ts-expect-error import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'; -import { type NodePath } from '@babel/traverse'; +import { type NodePath, type Visitor } from '@babel/traverse'; +import ResolveType from '@vue/babel-plugin-resolve-type'; +import { declare } from '@babel/helper-plugin-utils'; import transformVueJSX from './transform-vue-jsx'; import sugarFragment from './sugar-fragment'; import type { State, VueJSXPluginOptions } from './interface'; @@ -31,181 +33,194 @@ const hasJSX = (parentPath: NodePath) => { const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; -export default ({ types }: typeof BabelCore): BabelCore.PluginObj => ({ - name: 'babel-plugin-jsx', - inherits: syntaxJsx, - visitor: { - ...transformVueJSX, - ...sugarFragment, - Program: { - enter(path, state) { - if (hasJSX(path)) { - const importNames = [ - 'createVNode', - 'Fragment', - 'resolveComponent', - 'withDirectives', - 'vShow', - 'vModelSelect', - 'vModelText', - 'vModelCheckbox', - 'vModelRadio', - 'vModelText', - 'vModelDynamic', - 'resolveDirective', - 'mergeProps', - 'createTextVNode', - 'isVNode', - ]; - if (isModule(path)) { - // import { createVNode } from "vue"; - const importMap: Record = {}; - importNames.forEach((name) => { - state.set(name, () => { - if (importMap[name]) { - return types.cloneNode(importMap[name]); - } - const identifier = addNamed(path, name, 'vue', { - ensureLiveReference: true, +export default declare>( + (api, opt, dirname) => { + const { types } = api; + let resolveType: BabelCore.PluginObj | undefined; + if (opt.resolveType !== false) { + if (typeof opt.resolveType === 'boolean') opt.resolveType = {}; + resolveType = ResolveType(api, opt.resolveType, dirname); + } + return { + ...(resolveType || {}), + name: 'babel-plugin-jsx', + inherits: syntaxJsx, + visitor: { + ...(resolveType?.visitor as Visitor), + ...transformVueJSX, + ...sugarFragment, + Program: { + enter(path, state) { + if (hasJSX(path)) { + const importNames = [ + 'createVNode', + 'Fragment', + 'resolveComponent', + 'withDirectives', + 'vShow', + 'vModelSelect', + 'vModelText', + 'vModelCheckbox', + 'vModelRadio', + 'vModelText', + 'vModelDynamic', + 'resolveDirective', + 'mergeProps', + 'createTextVNode', + 'isVNode', + ]; + if (isModule(path)) { + // import { createVNode } from "vue"; + const importMap: Record = {}; + importNames.forEach((name) => { + state.set(name, () => { + if (importMap[name]) { + return types.cloneNode(importMap[name]); + } + const identifier = addNamed(path, name, 'vue', { + ensureLiveReference: true, + }); + importMap[name] = identifier; + return identifier; + }); }); - importMap[name] = identifier; - return identifier; - }); - }); - const { enableObjectSlots = true } = state.opts; - if (enableObjectSlots) { - state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { - if (importMap.runtimeIsSlot) { - return importMap.runtimeIsSlot; - } - const { name: isVNodeName } = state.get( - 'isVNode' - )() as t.Identifier; - const isSlot = path.scope.generateUidIdentifier('isSlot'); - const ast = template.ast` - function ${isSlot.name}(s) { - return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); - } - `; - const lastImport = (path.get('body') as NodePath[]) - .filter((p) => p.isImportDeclaration()) - .pop(); - if (lastImport) { - lastImport.insertAfter(ast); - } - importMap.runtimeIsSlot = isSlot; - return isSlot; - }); - } - } else { - // var _vue = require('vue'); - let sourceName: t.Identifier; - importNames.forEach((name) => { - state.set(name, () => { - if (!sourceName) { - sourceName = addNamespace(path, 'vue', { - ensureLiveReference: true, + const { enableObjectSlots = true } = state.opts; + if (enableObjectSlots) { + state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { + if (importMap.runtimeIsSlot) { + return importMap.runtimeIsSlot; + } + const { name: isVNodeName } = state.get( + 'isVNode' + )() as t.Identifier; + const isSlot = path.scope.generateUidIdentifier('isSlot'); + const ast = template.ast` + function ${isSlot.name}(s) { + return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); + } + `; + const lastImport = (path.get('body') as NodePath[]) + .filter((p) => p.isImportDeclaration()) + .pop(); + if (lastImport) { + lastImport.insertAfter(ast); + } + importMap.runtimeIsSlot = isSlot; + return isSlot; }); } - return t.memberExpression(sourceName, t.identifier(name)); - }); - }); + } else { + // var _vue = require('vue'); + let sourceName: t.Identifier; + importNames.forEach((name) => { + state.set(name, () => { + if (!sourceName) { + sourceName = addNamespace(path, 'vue', { + ensureLiveReference: true, + }); + } + return t.memberExpression(sourceName, t.identifier(name)); + }); + }); - const helpers: Record = {}; + const helpers: Record = {}; - const { enableObjectSlots = true } = state.opts; - if (enableObjectSlots) { - state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { - if (helpers.runtimeIsSlot) { - return helpers.runtimeIsSlot; - } - const isSlot = path.scope.generateUidIdentifier('isSlot'); - const { object: objectName } = state.get( - 'isVNode' - )() as t.MemberExpression; - const ast = template.ast` - function ${isSlot.name}(s) { - return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ - (objectName as t.Identifier).name - }.isVNode(s)); - } - `; + const { enableObjectSlots = true } = state.opts; + if (enableObjectSlots) { + state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { + if (helpers.runtimeIsSlot) { + return helpers.runtimeIsSlot; + } + const isSlot = path.scope.generateUidIdentifier('isSlot'); + const { object: objectName } = state.get( + 'isVNode' + )() as t.MemberExpression; + const ast = template.ast` + function ${isSlot.name}(s) { + return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ + (objectName as t.Identifier).name + }.isVNode(s)); + } + `; - const nodePaths = path.get('body') as NodePath[]; - const lastImport = nodePaths - .filter( - (p) => - p.isVariableDeclaration() && - p.node.declarations.some( - (d) => (d.id as t.Identifier)?.name === sourceName.name + const nodePaths = path.get('body') as NodePath[]; + const lastImport = nodePaths + .filter( + (p) => + p.isVariableDeclaration() && + p.node.declarations.some( + (d) => + (d.id as t.Identifier)?.name === sourceName.name + ) ) - ) - .pop(); - if (lastImport) { - lastImport.insertAfter(ast); + .pop(); + if (lastImport) { + lastImport.insertAfter(ast); + } + return isSlot; + }); } - return isSlot; - }); - } - } - - const { - opts: { pragma = '' }, - file, - } = state; + } - if (pragma) { - state.set('createVNode', () => t.identifier(pragma)); - } + const { + opts: { pragma = '' }, + file, + } = state; - if (file.ast.comments) { - for (const comment of file.ast.comments) { - const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value); - if (jsxMatches) { - state.set('createVNode', () => t.identifier(jsxMatches[1])); + if (pragma) { + state.set('createVNode', () => t.identifier(pragma)); } - } - } - } - }, - exit(path) { - const body = path.get('body') as NodePath[]; - const specifiersMap = new Map(); - body - .filter( - (nodePath) => - t.isImportDeclaration(nodePath.node) && - nodePath.node.source.value === 'vue' - ) - .forEach((nodePath) => { - const { specifiers } = nodePath.node as t.ImportDeclaration; - let shouldRemove = false; - specifiers.forEach((specifier) => { - if ( - !specifier.loc && - t.isImportSpecifier(specifier) && - t.isIdentifier(specifier.imported) - ) { - specifiersMap.set(specifier.imported.name, specifier); - shouldRemove = true; + if (file.ast.comments) { + for (const comment of file.ast.comments) { + const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value); + if (jsxMatches) { + state.set('createVNode', () => t.identifier(jsxMatches[1])); + } + } } - }); - if (shouldRemove) { - nodePath.remove(); } - }); + }, + exit(path) { + const body = path.get('body') as NodePath[]; + const specifiersMap = new Map(); - const specifiers = [...specifiersMap.keys()].map( - (imported) => specifiersMap.get(imported)! - ); - if (specifiers.length) { - path.unshiftContainer( - 'body', - t.importDeclaration(specifiers, t.stringLiteral('vue')) - ); - } + body + .filter( + (nodePath) => + t.isImportDeclaration(nodePath.node) && + nodePath.node.source.value === 'vue' + ) + .forEach((nodePath) => { + const { specifiers } = nodePath.node as t.ImportDeclaration; + let shouldRemove = false; + specifiers.forEach((specifier) => { + if ( + !specifier.loc && + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported) + ) { + specifiersMap.set(specifier.imported.name, specifier); + shouldRemove = true; + } + }); + if (shouldRemove) { + nodePath.remove(); + } + }); + + const specifiers = [...specifiersMap.keys()].map( + (imported) => specifiersMap.get(imported)! + ); + if (specifiers.length) { + path.unshiftContainer( + 'body', + t.importDeclaration(specifiers, t.stringLiteral('vue')) + ); + } + }, + }, }, - }, - }, -}); + }; + } +); diff --git a/packages/babel-plugin-jsx/src/interface.ts b/packages/babel-plugin-jsx/src/interface.ts index 6eea6b0a..4700e5b6 100644 --- a/packages/babel-plugin-jsx/src/interface.ts +++ b/packages/babel-plugin-jsx/src/interface.ts @@ -1,5 +1,6 @@ import type * as t from '@babel/types'; import type * as BabelCore from '@babel/core'; +import { type Options } from '@vue/babel-plugin-resolve-type'; export type Slots = t.Identifier | t.ObjectExpression | null; @@ -23,4 +24,8 @@ export interface VueJSXPluginOptions { enableObjectSlots?: boolean; /** Replace the function used when compiling JSX expressions */ pragma?: string; + /** + * enabled by default + */ + resolveType?: Options | boolean; } diff --git a/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap b/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap new file mode 100644 index 00000000..8058483a --- /dev/null +++ b/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`resolve type > runtime props > basic 1`] = ` +"import { createVNode as _createVNode } from \\"vue\\"; +interface Props { + foo?: string; +} +const App = defineComponent((props: Props) => _createVNode(\\"div\\", null, null), { + props: { + foo: { + type: String, + required: false + } + }, + name: \\"App\\" +});" +`; diff --git a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap index ae1175c5..01cfc648 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap @@ -188,6 +188,8 @@ const A = defineComponent({ }) { return () => _createVNode(\\"span\\", null, [slots.default()]); } +}, { + name: \\"A\\" }); const _a2 = 2; a = _a2; diff --git a/packages/babel-plugin-jsx/test/resolve-type.test.tsx b/packages/babel-plugin-jsx/test/resolve-type.test.tsx new file mode 100644 index 00000000..83926d4f --- /dev/null +++ b/packages/babel-plugin-jsx/test/resolve-type.test.tsx @@ -0,0 +1,21 @@ +import { transformAsync } from '@babel/core'; +// @ts-expect-error missing types +import typescript from '@babel/plugin-syntax-typescript'; +import VueJsx from '../src'; + +describe('resolve type', () => { + describe('runtime props', () => { + test('basic', async () => { + const result = await transformAsync( + ` + interface Props { foo?: string } + const App = defineComponent((props: Props) =>
) + `, + { + plugins: [[typescript, { isTSX: true }], VueJsx], + } + ); + expect(result!.code).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/babel-plugin-resolve-type/package.json b/packages/babel-plugin-resolve-type/package.json index b0956e08..374ccd6e 100644 --- a/packages/babel-plugin-resolve-type/package.json +++ b/packages/babel-plugin-resolve-type/package.json @@ -35,14 +35,15 @@ "dependencies": { "@babel/code-frame": "^7.22.10", "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", "@babel/parser": "^7.22.11", "@vue/compiler-sfc": "npm:@vue/compiler-sfc-canary@minor" }, "devDependencies": { "@babel/core": "^7.22.11", - "@babel/plugin-syntax-typescript": "^7.22.5", "@types/babel__code-frame": "^7.0.3", "@types/babel__helper-module-imports": "^7.18.0", + "@types/babel__helper-plugin-utils": "^7.10.1", "vue": "^3.3.4" } } diff --git a/packages/babel-plugin-resolve-type/src/index.ts b/packages/babel-plugin-resolve-type/src/index.ts index 82ba5768..39d0e76c 100644 --- a/packages/babel-plugin-resolve-type/src/index.ts +++ b/packages/babel-plugin-resolve-type/src/index.ts @@ -8,10 +8,11 @@ import { } from '@vue/compiler-sfc'; import { codeFrameColumns } from '@babel/code-frame'; import { addNamed } from '@babel/helper-module-imports'; +import { declare } from '@babel/helper-plugin-utils'; -export default ({ - types: t, -}: typeof BabelCore): BabelCore.PluginObj => { +export { SimpleTypeResolveOptions as Options }; + +export default declare(({ types: t }, options) => { let ctx: SimpleTypeResolveContext | undefined; let helpers: Set | undefined; @@ -23,7 +24,7 @@ export default ({ ctx = { filename: filename, source: file.code, - options: this || {}, + options, ast: file.ast.program.body, error(msg, node) { throw new Error( @@ -178,7 +179,7 @@ export default ({ t.objectProperty(t.identifier('emits'), ast) ); } -}; +}); function getTypeAnnotation(node: BabelCore.types.Node) { if ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cdfdd64..adc583fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@babel/plugin-syntax-typescript': + specifier: ^7.22.5 + version: 7.22.5(@babel/core@7.22.20) '@rollup/plugin-babel': specifier: ^6.0.3 version: 6.0.3(@babel/core@7.22.20)(@types/babel__core@7.20.2) @@ -61,6 +64,9 @@ importers: '@babel/helper-module-imports': specifier: ^7.22.15 version: 7.22.15 + '@babel/helper-plugin-utils': + specifier: ^7.22.5 + version: 7.22.5 '@babel/plugin-syntax-jsx': specifier: ^7.22.5 version: 7.22.5(@babel/core@7.22.20) @@ -76,6 +82,9 @@ importers: '@vue/babel-helper-vue-transform-on': specifier: workspace:^ version: link:../babel-helper-vue-transform-on + '@vue/babel-plugin-resolve-type': + specifier: workspace:^ + version: link:../babel-plugin-resolve-type camelcase: specifier: ^6.3.0 version: 6.3.0 @@ -92,6 +101,9 @@ importers: '@babel/preset-env': specifier: ^7.22.20 version: 7.22.20(@babel/core@7.22.20) + '@types/babel__helper-plugin-utils': + specifier: ^7.10.1 + version: 7.10.1 '@types/babel__template': specifier: ^7.4.2 version: 7.4.2 @@ -122,6 +134,9 @@ importers: '@babel/helper-module-imports': specifier: ^7.22.5 version: 7.22.15 + '@babel/helper-plugin-utils': + specifier: ^7.22.5 + version: 7.22.5 '@babel/parser': specifier: ^7.22.11 version: 7.22.16 @@ -132,15 +147,15 @@ importers: '@babel/core': specifier: ^7.22.11 version: 7.22.20 - '@babel/plugin-syntax-typescript': - specifier: ^7.22.5 - version: 7.22.5(@babel/core@7.22.20) '@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 + '@types/babel__helper-plugin-utils': + specifier: ^7.10.1 + version: 7.10.1 vue: specifier: ^3.3.4 version: 3.3.4 @@ -1797,6 +1812,12 @@ packages: '@types/babel__traverse': 7.20.2 dev: true + /@types/babel__helper-plugin-utils@7.10.1: + resolution: {integrity: sha512-6RaT7i6r2rT6ouIDZ2Cd6dPkq4wn1F8pLyDO+7wPVsL1dodvORiZORImaD6j9FBcHjPGuERE0hhtwkuPNXsO0A==} + dependencies: + '@types/babel__core': 7.20.2 + dev: true + /@types/babel__template@7.4.2: resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} dependencies: