mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-07-05 19:47:37 +08:00
feat: resolve TS type (#664)
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vue/babel-plugin-jsx",
|
||||
"version": "1.1.6",
|
||||
"version": "1.2.0-alpha.0",
|
||||
"description": "Babel plugin for Vue 3 JSX",
|
||||
"author": "Amour1688 <lcz_1996@foxmail.com>",
|
||||
"homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-jsx#readme",
|
||||
@ -24,11 +24,13 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.22.15",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
"@babel/plugin-syntax-jsx": "^7.23.3",
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/traverse": "^7.23.7",
|
||||
"@babel/types": "^7.23.6",
|
||||
"@vue/babel-helper-vue-transform-on": "workspace:^",
|
||||
"@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"
|
||||
|
@ -3,9 +3,10 @@ import type * as BabelCore from '@babel/core';
|
||||
import _template from '@babel/template';
|
||||
// @ts-expect-error
|
||||
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';
|
||||
@ -39,181 +40,197 @@ function interopDefault(m: any) {
|
||||
const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx);
|
||||
const template = /*#__PURE__*/ interopDefault(_template);
|
||||
|
||||
export default ({ types }: typeof BabelCore): BabelCore.PluginObj<State> => ({
|
||||
name: 'babel-plugin-jsx',
|
||||
inherits: /*#__PURE__*/ interopDefault(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<string, t.Identifier> = {};
|
||||
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<VueJSXPluginOptions, BabelCore.PluginObj<State>>(
|
||||
(api, opt, dirname) => {
|
||||
const { types } = api;
|
||||
let resolveType: BabelCore.PluginObj<BabelCore.PluginPass> | 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: /*#__PURE__*/ interopDefault(syntaxJsx),
|
||||
visitor: {
|
||||
...(resolveType?.visitor as Visitor<State>),
|
||||
...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<
|
||||
string,
|
||||
t.MemberExpression | t.Identifier
|
||||
> = {};
|
||||
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<string, t.Identifier> = {};
|
||||
const helpers: Record<string, t.Identifier> = {};
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
opts: { pragma = '' },
|
||||
file,
|
||||
} = state;
|
||||
|
||||
if (pragma) {
|
||||
state.set('createVNode', () => t.identifier(pragma));
|
||||
}
|
||||
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
exit(path) {
|
||||
const body = path.get('body') as NodePath[];
|
||||
const specifiersMap = new Map<string, t.ImportSpecifier>();
|
||||
|
||||
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();
|
||||
}
|
||||
return isSlot;
|
||||
});
|
||||
|
||||
const specifiers = [...specifiersMap.keys()].map(
|
||||
(imported) => specifiersMap.get(imported)!
|
||||
);
|
||||
if (specifiers.length) {
|
||||
path.unshiftContainer(
|
||||
'body',
|
||||
t.importDeclaration(specifiers, t.stringLiteral('vue'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
opts: { pragma = '' },
|
||||
file,
|
||||
} = state;
|
||||
|
||||
if (pragma) {
|
||||
state.set('createVNode', () => t.identifier(pragma));
|
||||
}
|
||||
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
exit(path) {
|
||||
const body = path.get('body') as NodePath[];
|
||||
const specifiersMap = new Map<string, t.ImportSpecifier>();
|
||||
|
||||
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'))
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as t from '@babel/types';
|
||||
import { type NodePath, type Visitor } from '@babel/traverse';
|
||||
// @ts-expect-error
|
||||
import { addDefault } from '@babel/helper-module-imports';
|
||||
import {
|
||||
buildIIFE,
|
||||
|
@ -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"
|
||||
});"
|
||||
`;
|
@ -188,6 +188,8 @@ const A = defineComponent({
|
||||
}) {
|
||||
return () => _createVNode("span", null, [slots.default()]);
|
||||
}
|
||||
}, {
|
||||
name: "A"
|
||||
});
|
||||
const _a2 = 2;
|
||||
a = _a2;
|
||||
|
21
packages/babel-plugin-jsx/test/resolve-type.test.tsx
Normal file
21
packages/babel-plugin-jsx/test/resolve-type.test.tsx
Normal file
@ -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) => <div />)
|
||||
`,
|
||||
{
|
||||
plugins: [[typescript, { isTSX: true }], VueJsx],
|
||||
}
|
||||
);
|
||||
expect(result!.code).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user