feat: resolve type

This commit is contained in:
三咲智子 Kevin Deng
2023-08-27 23:47:23 +08:00
parent 4050dd6da4
commit a8d9d1253a
7 changed files with 498 additions and 1 deletions

View File

@ -0,0 +1 @@
# babel-plugin-resolve-type

View File

@ -0,0 +1,41 @@
{
"name": "@vue/babel-plugin-resolve-type",
"version": "0.0.0",
"description": "Babel plugin for resolving Vue types",
"author": "三咲智子 <sxzz@sxzz.moe>",
"homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-resolve-type#readme",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/babel-plugin-jsx"
},
"scripts": {
"build": "tsup",
"watch": "tsup --watch"
},
"bugs": {
"url": "https://github.com/vuejs/babel-plugin-jsx/issues"
},
"files": [
"dist"
],
"peerDependencies": {
"@babel/core": "^7.0.0-0"
},
"dependencies": {
"@babel/code-frame": "^7.22.10",
"@babel/helper-module-imports": "^7.22.5",
"@babel/parser": "^7.22.11",
"@babel/plugin-syntax-typescript": "^7.22.5",
"@vue/compiler-sfc": "link:/Users/kevin/Developer/open-source/vue/vue-core/packages/compiler-sfc"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@types/babel__code-frame": "^7.0.3",
"@types/babel__helper-module-imports": "^7.18.0",
"vue": "^3.3.4"
}
}

View File

@ -0,0 +1,161 @@
import type * as BabelCore from '@babel/core';
import { parseExpression } from '@babel/parser';
// @ts-expect-error no dts
import typescript from '@babel/plugin-syntax-typescript';
import {
type SFCScriptCompileOptions,
type SimpleTypeResolveContext,
extractRuntimeEmits,
extractRuntimeProps,
} from '@vue/compiler-sfc';
import { codeFrameColumns } from '@babel/code-frame';
import { addNamed } from '@babel/helper-module-imports';
export interface Options {
compileOptions?: SFCScriptCompileOptions;
}
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<Options> => {
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' && 'typeAnnotation' in props.left) {
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,
pre(file) {
const filename = file.opts.filename || 'unknown.js';
helpers = new Set();
ctx = {
filename: filename,
source: file.code,
options: this.compileOptions || {},
ast: file.ast.program.body as any,
error(msg, node) {
throw new Error(
`[@vue/babel-plugin-resolve-type] ${msg}\n\n${filename}\n${codeFrameColumns(
file.code,
{
start: {
line: node.loc!.start.line,
column: node.loc!.start.column + 1,
},
end: {
line: node.loc!.end.line,
column: node.loc!.end.column + 1,
},
}
)}`
);
},
helper(key) {
helpers!.add(key);
return `_${key}`;
},
getString(node) {
return file.code.slice(node.start!, node.end!);
},
bindingMetadata: Object.create(null),
propsTypeDecl: undefined,
propsRuntimeDefaults: undefined,
propsDestructuredBindings: {},
emitsTypeDecl: undefined,
};
},
visitor: {
CallExpression(path) {
if (!ctx) {
throw new Error(
'[@vue/babel-plugin-resolve-type] context is not loaded.'
);
}
const node = path.node;
if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return;
const comp = node.arguments[0];
if (!comp || !t.isFunction(comp)) return;
let options = node.arguments[1];
if (!options) {
options = t.objectExpression([]);
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);
},
},
post(file) {
for (const helper of helpers!) {
addNamed(file.path, `_${helper}`, 'vue');
}
},
};
};

View File

@ -0,0 +1,82 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`resolve type > runtime emits > basic 1`] = `
"import { type SetupContext, defineComponent } from 'vue';
const Comp = defineComponent((props, {
emit
}: SetupContext<{
change(val: string): void;
click(): void;
}>) => {
emit('change');
return () => {};
}, {
emits: [\\"change\\", \\"click\\"]
});"
`;
exports[`resolve type > runtime props > basic 1`] = `
"import { defineComponent, h } from 'vue';
interface Props {
msg: string;
optional?: boolean;
}
interface Props2 {
set: Set<string>;
}
defineComponent((props: Props & Props2) => {
return () => h('div', props.msg);
}, {
props: {
msg: {
type: String,
required: true
},
optional: {
type: Boolean,
required: false
},
set: {
type: Set,
required: true
}
}
});"
`;
exports[`resolve type > runtime props > with dynamic default value 1`] = `
"import { _mergeDefaults } from \\"vue\\";
import { defineComponent, h } from 'vue';
const defaults = {};
defineComponent((props: {
msg?: string;
} = defaults) => {
return () => h('div', props.msg);
}, {
props: _mergeDefaults({
msg: {
type: String,
required: false
}
}, defaults)
});"
`;
exports[`resolve type > runtime props > with static default value 1`] = `
"import { defineComponent, h } from 'vue';
defineComponent((props: {
msg?: string;
} = {
msg: 'hello'
}) => {
return () => h('div', props.msg);
}, {
props: {
msg: {
type: String,
required: false,
default: 'hello'
}
}
});"
`;

View File

@ -0,0 +1,75 @@
import { transformAsync } from '@babel/core';
import ResolveType from '../src';
async function transform(code: string): Promise<string> {
const result = await transformAsync(code, { plugins: [ResolveType] });
return result!.code!;
}
describe('resolve type', () => {
describe('runtime props', () => {
test('basic', async () => {
const result = await transform(
`
import { defineComponent, h } from 'vue';
interface Props {
msg: string;
optional?: boolean;
}
interface Props2 {
set: Set<string>;
}
defineComponent((props: Props & Props2) => {
return () => h('div', props.msg);
})
`
);
expect(result).toMatchSnapshot();
});
test('with static default value', async () => {
const result = await transform(
`
import { defineComponent, h } from 'vue';
defineComponent((props: { msg?: string } = { msg: 'hello' }) => {
return () => h('div', props.msg);
})
`
);
expect(result).toMatchSnapshot();
});
test('with dynamic default value', async () => {
const result = await transform(
`
import { defineComponent, h } from 'vue';
const defaults = {}
defineComponent((props: { msg?: string } = defaults) => {
return () => h('div', props.msg);
})
`
);
expect(result).toMatchSnapshot();
});
});
describe('runtime emits', () => {
test('basic', async () => {
const result = await transform(
`
import { type SetupContext, defineComponent } from 'vue';
const Comp = defineComponent(
(
props,
{ emit }: SetupContext<{ change(val: string): void; click(): void }>
) => {
emit('change');
return () => {};
}
);
`
);
expect(result).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,9 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
target: 'node14',
platform: 'neutral',
});