mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-07-05 11:33:30 +08:00
feat: infer component name
This commit is contained in:
@ -11,71 +11,12 @@ import {
|
|||||||
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';
|
||||||
|
|
||||||
function getTypeAnnotation(node: BabelCore.types.Node) {
|
|
||||||
if (
|
|
||||||
'typeAnnotation' in node &&
|
|
||||||
node.typeAnnotation &&
|
|
||||||
node.typeAnnotation.type === 'TSTypeAnnotation'
|
|
||||||
) {
|
|
||||||
return node.typeAnnotation.typeAnnotation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
types: t,
|
types: t,
|
||||||
}: typeof BabelCore): BabelCore.PluginObj<SimpleTypeResolveOptions> => {
|
}: typeof BabelCore): BabelCore.PluginObj<SimpleTypeResolveOptions> => {
|
||||||
let ctx: SimpleTypeResolveContext | undefined;
|
let ctx: SimpleTypeResolveContext | undefined;
|
||||||
let helpers: Set<string> | undefined;
|
let helpers: Set<string> | 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 {
|
return {
|
||||||
name: 'babel-plugin-resolve-type',
|
name: 'babel-plugin-resolve-type',
|
||||||
inherits: typescript,
|
inherits: typescript,
|
||||||
@ -125,8 +66,10 @@ export default ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = path.node;
|
const { node } = path;
|
||||||
|
|
||||||
if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return;
|
if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return;
|
||||||
|
if (!checkDefineComponent(path)) return;
|
||||||
|
|
||||||
const comp = node.arguments[0];
|
const comp = node.arguments[0];
|
||||||
if (!comp || !t.isFunction(comp)) return;
|
if (!comp || !t.isFunction(comp)) return;
|
||||||
@ -137,14 +80,11 @@ export default ({
|
|||||||
node.arguments.push(options);
|
node.arguments.push(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!t.isObjectExpression(options)) {
|
node.arguments[1] = processProps(comp, options) || options;
|
||||||
throw new Error(
|
node.arguments[1] = processEmits(comp, node.arguments[1]) || options;
|
||||||
'[@vue/babel-plugin-resolve-type] Options inside of defineComponent should be an object expression.'
|
},
|
||||||
);
|
VariableDeclarator(path) {
|
||||||
}
|
inferComponentName(path);
|
||||||
|
|
||||||
processProps(comp, options);
|
|
||||||
processEmits(comp, options);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
post(file) {
|
post(file) {
|
||||||
@ -153,4 +93,128 @@ export default ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function inferComponentName(
|
||||||
|
path: BabelCore.NodePath<BabelCore.types.VariableDeclarator>
|
||||||
|
) {
|
||||||
|
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<BabelCore.types.CallExpression>
|
||||||
|
) {
|
||||||
|
const defineCompImport =
|
||||||
|
path.scope.getBinding('defineComponent')?.path.parent;
|
||||||
|
if (!defineCompImport) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
defineCompImport.type === 'ImportDeclaration' &&
|
||||||
|
/^@?vue(\/|$)/.test(defineCompImport.source.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProperty<T extends BabelCore.types.Node>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,76 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`resolve type > defineComponent scope > fake 1`] = `
|
||||||
|
"const defineComponent = () => {};
|
||||||
|
defineComponent((props: {
|
||||||
|
msg?: string;
|
||||||
|
}) => {
|
||||||
|
return () => <div />;
|
||||||
|
});"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`resolve type > defineComponent scope > import sub-package 1`] = `
|
||||||
|
"import { defineComponent } from 'vue/dist/vue.esm-bundler';
|
||||||
|
defineComponent((props: {
|
||||||
|
msg?: string;
|
||||||
|
}) => {
|
||||||
|
return () => <div />;
|
||||||
|
}, {
|
||||||
|
props: {
|
||||||
|
msg: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`resolve type > defineComponent scope > w/o import 1`] = `
|
||||||
|
"defineComponent((props: {
|
||||||
|
msg?: string;
|
||||||
|
}) => {
|
||||||
|
return () => <div />;
|
||||||
|
}, {
|
||||||
|
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`] = `
|
exports[`resolve type > runtime emits > basic 1`] = `
|
||||||
"import { type SetupContext, defineComponent } from 'vue';
|
"import { type SetupContext, defineComponent } from 'vue';
|
||||||
const Comp = defineComponent((props, {
|
defineComponent((props, {
|
||||||
emit
|
emit
|
||||||
}: SetupContext<{
|
}: SetupContext<{
|
||||||
change(val: string): void;
|
change(val: string): void;
|
||||||
@ -83,7 +151,7 @@ defineComponent((props: {
|
|||||||
|
|
||||||
exports[`resolve type > w/ tsx 1`] = `
|
exports[`resolve type > w/ tsx 1`] = `
|
||||||
"import { type SetupContext, defineComponent } from 'vue';
|
"import { type SetupContext, defineComponent } from 'vue';
|
||||||
const Comp = defineComponent(() => {
|
defineComponent(() => {
|
||||||
return () => <div />;
|
return () => <div />;
|
||||||
}, {});"
|
}, {});"
|
||||||
`;
|
`;
|
||||||
|
@ -60,7 +60,7 @@ describe('resolve type', () => {
|
|||||||
const result = await transform(
|
const result = await transform(
|
||||||
`
|
`
|
||||||
import { type SetupContext, defineComponent } from 'vue';
|
import { type SetupContext, defineComponent } from 'vue';
|
||||||
const Comp = defineComponent(
|
defineComponent(
|
||||||
(
|
(
|
||||||
props,
|
props,
|
||||||
{ emit }: SetupContext<{ change(val: string): void; click(): void }>
|
{ emit }: SetupContext<{ change(val: string): void; click(): void }>
|
||||||
@ -79,7 +79,43 @@ describe('resolve type', () => {
|
|||||||
const result = await transform(
|
const result = await transform(
|
||||||
`
|
`
|
||||||
import { type SetupContext, defineComponent } from 'vue';
|
import { type SetupContext, defineComponent } from 'vue';
|
||||||
const Comp = defineComponent(() => {
|
defineComponent(() => {
|
||||||
|
return () => <div/ >;
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('defineComponent scope', () => {
|
||||||
|
test('fake', async () => {
|
||||||
|
const result = await transform(
|
||||||
|
`
|
||||||
|
const defineComponent = () => {};
|
||||||
|
defineComponent((props: { msg?: string }) => {
|
||||||
|
return () => <div/ >;
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('w/o import', async () => {
|
||||||
|
const result = await transform(
|
||||||
|
`
|
||||||
|
defineComponent((props: { msg?: string }) => {
|
||||||
|
return () => <div/ >;
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
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 () => <div/ >;
|
return () => <div/ >;
|
||||||
});
|
});
|
||||||
`
|
`
|
||||||
@ -87,3 +123,46 @@ describe('resolve type', () => {
|
|||||||
expect(result).toMatchSnapshot();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user