feat: integrate with JSX plugin

This commit is contained in:
三咲智子 Kevin Deng 2023-09-26 01:53:05 +08:00
parent 3a3c4f85c0
commit 6760d0b459
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
10 changed files with 264 additions and 177 deletions

View File

@ -15,6 +15,7 @@
"jsx" "jsx"
], ],
"devDependencies": { "devDependencies": {
"@babel/plugin-syntax-typescript": "^7.22.5",
"@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-babel": "^6.0.3",
"@types/babel__core": "^7.20.2", "@types/babel__core": "^7.20.2",
"@types/node": "^20.6.5", "@types/node": "^20.6.5",

View File

@ -24,11 +24,13 @@
], ],
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.22.15", "@babel/helper-module-imports": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-jsx": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5",
"@babel/template": "^7.22.15", "@babel/template": "^7.22.15",
"@babel/traverse": "^7.22.20", "@babel/traverse": "^7.22.20",
"@babel/types": "^7.22.19", "@babel/types": "^7.22.19",
"@vue/babel-helper-vue-transform-on": "workspace:^", "@vue/babel-helper-vue-transform-on": "workspace:^",
"@vue/babel-plugin-resolve-type": "workspace:^",
"camelcase": "^6.3.0", "camelcase": "^6.3.0",
"html-tags": "^3.3.1", "html-tags": "^3.3.1",
"svg-tags": "^1.0.0" "svg-tags": "^1.0.0"
@ -36,6 +38,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.20", "@babel/core": "^7.22.20",
"@babel/preset-env": "^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__template": "^7.4.2",
"@types/babel__traverse": "^7.20.2", "@types/babel__traverse": "^7.20.2",
"@types/svg-tags": "^1.0.0", "@types/svg-tags": "^1.0.0",

View File

@ -5,7 +5,9 @@ import template from '@babel/template';
import syntaxJsx from '@babel/plugin-syntax-jsx'; import syntaxJsx from '@babel/plugin-syntax-jsx';
// @ts-expect-error // @ts-expect-error
import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'; 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 transformVueJSX from './transform-vue-jsx';
import sugarFragment from './sugar-fragment'; import sugarFragment from './sugar-fragment';
import type { State, VueJSXPluginOptions } from './interface'; import type { State, VueJSXPluginOptions } from './interface';
@ -31,181 +33,194 @@ const hasJSX = (parentPath: NodePath<t.Program>) => {
const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/;
export default ({ types }: typeof BabelCore): BabelCore.PluginObj<State> => ({ export default declare<VueJSXPluginOptions, BabelCore.PluginObj<State>>(
name: 'babel-plugin-jsx', (api, opt, dirname) => {
inherits: syntaxJsx, const { types } = api;
visitor: { let resolveType: BabelCore.PluginObj<BabelCore.PluginPass> | undefined;
...transformVueJSX, if (opt.resolveType !== false) {
...sugarFragment, if (typeof opt.resolveType === 'boolean') opt.resolveType = {};
Program: { resolveType = ResolveType(api, opt.resolveType, dirname);
enter(path, state) { }
if (hasJSX(path)) { return {
const importNames = [ ...(resolveType || {}),
'createVNode', name: 'babel-plugin-jsx',
'Fragment', inherits: syntaxJsx,
'resolveComponent', visitor: {
'withDirectives', ...(resolveType?.visitor as Visitor<State>),
'vShow', ...transformVueJSX,
'vModelSelect', ...sugarFragment,
'vModelText', Program: {
'vModelCheckbox', enter(path, state) {
'vModelRadio', if (hasJSX(path)) {
'vModelText', const importNames = [
'vModelDynamic', 'createVNode',
'resolveDirective', 'Fragment',
'mergeProps', 'resolveComponent',
'createTextVNode', 'withDirectives',
'isVNode', 'vShow',
]; 'vModelSelect',
if (isModule(path)) { 'vModelText',
// import { createVNode } from "vue"; 'vModelCheckbox',
const importMap: Record<string, t.Identifier> = {}; 'vModelRadio',
importNames.forEach((name) => { 'vModelText',
state.set(name, () => { 'vModelDynamic',
if (importMap[name]) { 'resolveDirective',
return types.cloneNode(importMap[name]); 'mergeProps',
} 'createTextVNode',
const identifier = addNamed(path, name, 'vue', { 'isVNode',
ensureLiveReference: true, ];
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,
});
importMap[name] = identifier;
return identifier;
});
}); });
importMap[name] = identifier; const { enableObjectSlots = true } = state.opts;
return identifier; if (enableObjectSlots) {
}); state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
}); if (importMap.runtimeIsSlot) {
const { enableObjectSlots = true } = state.opts; return importMap.runtimeIsSlot;
if (enableObjectSlots) { }
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { const { name: isVNodeName } = state.get(
if (importMap.runtimeIsSlot) { 'isVNode'
return importMap.runtimeIsSlot; )() as t.Identifier;
} const isSlot = path.scope.generateUidIdentifier('isSlot');
const { name: isVNodeName } = state.get( const ast = template.ast`
'isVNode' function ${isSlot.name}(s) {
)() as t.Identifier; return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s));
const isSlot = path.scope.generateUidIdentifier('isSlot'); }
const ast = template.ast` `;
function ${isSlot.name}(s) { const lastImport = (path.get('body') as NodePath[])
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); .filter((p) => p.isImportDeclaration())
} .pop();
`; if (lastImport) {
const lastImport = (path.get('body') as NodePath[]) lastImport.insertAfter(ast);
.filter((p) => p.isImportDeclaration()) }
.pop(); importMap.runtimeIsSlot = isSlot;
if (lastImport) { return isSlot;
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,
}); });
} }
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; const { enableObjectSlots = true } = state.opts;
if (enableObjectSlots) { if (enableObjectSlots) {
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
if (helpers.runtimeIsSlot) { if (helpers.runtimeIsSlot) {
return helpers.runtimeIsSlot; return helpers.runtimeIsSlot;
} }
const isSlot = path.scope.generateUidIdentifier('isSlot'); const isSlot = path.scope.generateUidIdentifier('isSlot');
const { object: objectName } = state.get( const { object: objectName } = state.get(
'isVNode' 'isVNode'
)() as t.MemberExpression; )() as t.MemberExpression;
const ast = template.ast` const ast = template.ast`
function ${isSlot.name}(s) { function ${isSlot.name}(s) {
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${
(objectName as t.Identifier).name (objectName as t.Identifier).name
}.isVNode(s)); }.isVNode(s));
} }
`; `;
const nodePaths = path.get('body') as NodePath[]; const nodePaths = path.get('body') as NodePath[];
const lastImport = nodePaths const lastImport = nodePaths
.filter( .filter(
(p) => (p) =>
p.isVariableDeclaration() && p.isVariableDeclaration() &&
p.node.declarations.some( p.node.declarations.some(
(d) => (d.id as t.Identifier)?.name === sourceName.name (d) =>
(d.id as t.Identifier)?.name === sourceName.name
)
) )
) .pop();
.pop(); if (lastImport) {
if (lastImport) { lastImport.insertAfter(ast);
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'))
);
}
},
},
},
});

View File

@ -1,5 +1,6 @@
import type * as t from '@babel/types'; import type * as t from '@babel/types';
import type * as BabelCore from '@babel/core'; import type * as BabelCore from '@babel/core';
import { type Options } from '@vue/babel-plugin-resolve-type';
export type Slots = t.Identifier | t.ObjectExpression | null; export type Slots = t.Identifier | t.ObjectExpression | null;
@ -23,4 +24,8 @@ export interface VueJSXPluginOptions {
enableObjectSlots?: boolean; enableObjectSlots?: boolean;
/** Replace the function used when compiling JSX expressions */ /** Replace the function used when compiling JSX expressions */
pragma?: string; pragma?: string;
/**
* enabled by default
*/
resolveType?: Options | boolean;
} }

View File

@ -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\\"
});"
`;

View File

@ -188,6 +188,8 @@ const A = defineComponent({
}) { }) {
return () => _createVNode(\\"span\\", null, [slots.default()]); return () => _createVNode(\\"span\\", null, [slots.default()]);
} }
}, {
name: \\"A\\"
}); });
const _a2 = 2; const _a2 = 2;
a = _a2; a = _a2;

View 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();
});
});
});

View File

@ -35,14 +35,15 @@
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.22.10", "@babel/code-frame": "^7.22.10",
"@babel/helper-module-imports": "^7.22.5", "@babel/helper-module-imports": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/parser": "^7.22.11", "@babel/parser": "^7.22.11",
"@vue/compiler-sfc": "npm:@vue/compiler-sfc-canary@minor" "@vue/compiler-sfc": "npm:@vue/compiler-sfc-canary@minor"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.11", "@babel/core": "^7.22.11",
"@babel/plugin-syntax-typescript": "^7.22.5",
"@types/babel__code-frame": "^7.0.3", "@types/babel__code-frame": "^7.0.3",
"@types/babel__helper-module-imports": "^7.18.0", "@types/babel__helper-module-imports": "^7.18.0",
"@types/babel__helper-plugin-utils": "^7.10.1",
"vue": "^3.3.4" "vue": "^3.3.4"
} }
} }

View File

@ -8,10 +8,11 @@ import {
} from '@vue/compiler-sfc'; } from '@vue/compiler-sfc';
import { codeFrameColumns } from '@babel/code-frame'; import { codeFrameColumns } from '@babel/code-frame';
import { addNamed } from '@babel/helper-module-imports'; import { addNamed } from '@babel/helper-module-imports';
import { declare } from '@babel/helper-plugin-utils';
export default ({ export { SimpleTypeResolveOptions as Options };
types: t,
}: typeof BabelCore): BabelCore.PluginObj<SimpleTypeResolveOptions> => { export default declare<SimpleTypeResolveOptions>(({ types: t }, options) => {
let ctx: SimpleTypeResolveContext | undefined; let ctx: SimpleTypeResolveContext | undefined;
let helpers: Set<string> | undefined; let helpers: Set<string> | undefined;
@ -23,7 +24,7 @@ export default ({
ctx = { ctx = {
filename: filename, filename: filename,
source: file.code, source: file.code,
options: this || {}, options,
ast: file.ast.program.body, ast: file.ast.program.body,
error(msg, node) { error(msg, node) {
throw new Error( throw new Error(
@ -178,7 +179,7 @@ export default ({
t.objectProperty(t.identifier('emits'), ast) t.objectProperty(t.identifier('emits'), ast)
); );
} }
}; });
function getTypeAnnotation(node: BabelCore.types.Node) { function getTypeAnnotation(node: BabelCore.types.Node) {
if ( if (

27
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
devDependencies: devDependencies:
'@babel/plugin-syntax-typescript':
specifier: ^7.22.5
version: 7.22.5(@babel/core@7.22.20)
'@rollup/plugin-babel': '@rollup/plugin-babel':
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3(@babel/core@7.22.20)(@types/babel__core@7.20.2) version: 6.0.3(@babel/core@7.22.20)(@types/babel__core@7.20.2)
@ -61,6 +64,9 @@ importers:
'@babel/helper-module-imports': '@babel/helper-module-imports':
specifier: ^7.22.15 specifier: ^7.22.15
version: 7.22.15 version: 7.22.15
'@babel/helper-plugin-utils':
specifier: ^7.22.5
version: 7.22.5
'@babel/plugin-syntax-jsx': '@babel/plugin-syntax-jsx':
specifier: ^7.22.5 specifier: ^7.22.5
version: 7.22.5(@babel/core@7.22.20) version: 7.22.5(@babel/core@7.22.20)
@ -76,6 +82,9 @@ importers:
'@vue/babel-helper-vue-transform-on': '@vue/babel-helper-vue-transform-on':
specifier: workspace:^ specifier: workspace:^
version: link:../babel-helper-vue-transform-on version: link:../babel-helper-vue-transform-on
'@vue/babel-plugin-resolve-type':
specifier: workspace:^
version: link:../babel-plugin-resolve-type
camelcase: camelcase:
specifier: ^6.3.0 specifier: ^6.3.0
version: 6.3.0 version: 6.3.0
@ -92,6 +101,9 @@ importers:
'@babel/preset-env': '@babel/preset-env':
specifier: ^7.22.20 specifier: ^7.22.20
version: 7.22.20(@babel/core@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': '@types/babel__template':
specifier: ^7.4.2 specifier: ^7.4.2
version: 7.4.2 version: 7.4.2
@ -122,6 +134,9 @@ importers:
'@babel/helper-module-imports': '@babel/helper-module-imports':
specifier: ^7.22.5 specifier: ^7.22.5
version: 7.22.15 version: 7.22.15
'@babel/helper-plugin-utils':
specifier: ^7.22.5
version: 7.22.5
'@babel/parser': '@babel/parser':
specifier: ^7.22.11 specifier: ^7.22.11
version: 7.22.16 version: 7.22.16
@ -132,15 +147,15 @@ importers:
'@babel/core': '@babel/core':
specifier: ^7.22.11 specifier: ^7.22.11
version: 7.22.20 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': '@types/babel__code-frame':
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
'@types/babel__helper-module-imports': '@types/babel__helper-module-imports':
specifier: ^7.18.0 specifier: ^7.18.0
version: 7.18.0 version: 7.18.0
'@types/babel__helper-plugin-utils':
specifier: ^7.10.1
version: 7.10.1
vue: vue:
specifier: ^3.3.4 specifier: ^3.3.4
version: 3.3.4 version: 3.3.4
@ -1797,6 +1812,12 @@ packages:
'@types/babel__traverse': 7.20.2 '@types/babel__traverse': 7.20.2
dev: true 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: /@types/babel__template@7.4.2:
resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==} resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==}
dependencies: dependencies: