Skip to content

Commit

Permalink
feat: infer component name
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Sep 25, 2023
1 parent aa95451 commit 516685b
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 72 deletions.
200 changes: 132 additions & 68 deletions packages/babel-plugin-resolve-type/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,12 @@ import {
import { codeFrameColumns } from '@babel/code-frame';
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 ({
types: t,
}: typeof BabelCore): BabelCore.PluginObj<SimpleTypeResolveOptions> => {
let ctx: SimpleTypeResolveContext | 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 {
name: 'babel-plugin-resolve-type',
inherits: typescript,
Expand Down Expand Up @@ -125,8 +66,10 @@ export default ({
);
}

const node = path.node;
const { node } = path;

if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return;
if (!checkDefineComponent(path)) return;

const comp = node.arguments[0];
if (!comp || !t.isFunction(comp)) return;
Expand All @@ -137,14 +80,11 @@ export default ({
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);
node.arguments[1] = processProps(comp, options) || options;
node.arguments[1] = processEmits(comp, node.arguments[1]) || options;
},
VariableDeclarator(path) {
inferComponentName(path);
},
},
post(file) {
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,76 @@
// 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`] = `
"import { type SetupContext, defineComponent } from 'vue';
const Comp = defineComponent((props, {
defineComponent((props, {
emit
}: SetupContext<{
change(val: string): void;
Expand Down Expand Up @@ -83,7 +151,7 @@ defineComponent((props: {
exports[`resolve type > w/ tsx 1`] = `
"import { type SetupContext, defineComponent } from 'vue';
const Comp = defineComponent(() => {
defineComponent(() => {
return () => <div />;
}, {});"
`;
Loading

0 comments on commit 516685b

Please sign in to comment.