Skip to content

Commit a8d9d12

Browse files
committed
feat: resolve type
1 parent 4050dd6 commit a8d9d12

File tree

7 files changed

+498
-1
lines changed

7 files changed

+498
-1
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# babel-plugin-resolve-type
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@vue/babel-plugin-resolve-type",
3+
"version": "0.0.0",
4+
"description": "Babel plugin for resolving Vue types",
5+
"author": "三咲智子 <[email protected]>",
6+
"homepage": "https://github.com/vuejs/babel-plugin-jsx/tree/dev/packages/babel-plugin-resolve-type#readme",
7+
"license": "MIT",
8+
"main": "dist/index.js",
9+
"module": "dist/index.mjs",
10+
"types": "dist/index.d.ts",
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/vuejs/babel-plugin-jsx"
14+
},
15+
"scripts": {
16+
"build": "tsup",
17+
"watch": "tsup --watch"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/vuejs/babel-plugin-jsx/issues"
21+
},
22+
"files": [
23+
"dist"
24+
],
25+
"peerDependencies": {
26+
"@babel/core": "^7.0.0-0"
27+
},
28+
"dependencies": {
29+
"@babel/code-frame": "^7.22.10",
30+
"@babel/helper-module-imports": "^7.22.5",
31+
"@babel/parser": "^7.22.11",
32+
"@babel/plugin-syntax-typescript": "^7.22.5",
33+
"@vue/compiler-sfc": "link:/Users/kevin/Developer/open-source/vue/vue-core/packages/compiler-sfc"
34+
},
35+
"devDependencies": {
36+
"@babel/core": "^7.22.9",
37+
"@types/babel__code-frame": "^7.0.3",
38+
"@types/babel__helper-module-imports": "^7.18.0",
39+
"vue": "^3.3.4"
40+
}
41+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type * as BabelCore from '@babel/core';
2+
import { parseExpression } from '@babel/parser';
3+
// @ts-expect-error no dts
4+
import typescript from '@babel/plugin-syntax-typescript';
5+
import {
6+
type SFCScriptCompileOptions,
7+
type SimpleTypeResolveContext,
8+
extractRuntimeEmits,
9+
extractRuntimeProps,
10+
} from '@vue/compiler-sfc';
11+
import { codeFrameColumns } from '@babel/code-frame';
12+
import { addNamed } from '@babel/helper-module-imports';
13+
14+
export interface Options {
15+
compileOptions?: SFCScriptCompileOptions;
16+
}
17+
18+
function getTypeAnnotation(node: BabelCore.types.Node) {
19+
if (
20+
'typeAnnotation' in node &&
21+
node.typeAnnotation &&
22+
node.typeAnnotation.type === 'TSTypeAnnotation'
23+
) {
24+
return node.typeAnnotation.typeAnnotation;
25+
}
26+
}
27+
28+
export default ({
29+
types: t,
30+
}: typeof BabelCore): BabelCore.PluginObj<Options> => {
31+
let ctx: SimpleTypeResolveContext | undefined;
32+
let helpers: Set<string> | undefined;
33+
34+
function processProps(
35+
comp: BabelCore.types.Function,
36+
options: BabelCore.types.ObjectExpression
37+
) {
38+
const props = comp.params[0];
39+
if (!props) return;
40+
41+
if (props.type === 'AssignmentPattern' && 'typeAnnotation' in props.left) {
42+
ctx!.propsTypeDecl = getTypeAnnotation(props.left);
43+
ctx!.propsRuntimeDefaults = props.right;
44+
} else {
45+
ctx!.propsTypeDecl = getTypeAnnotation(props);
46+
}
47+
48+
if (!ctx!.propsTypeDecl) return;
49+
50+
const runtimeProps = extractRuntimeProps(ctx!);
51+
if (!runtimeProps) {
52+
return;
53+
}
54+
55+
const ast = parseExpression(runtimeProps);
56+
options.properties.push(t.objectProperty(t.identifier('props'), ast));
57+
}
58+
59+
function processEmits(
60+
comp: BabelCore.types.Function,
61+
options: BabelCore.types.ObjectExpression
62+
) {
63+
const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]);
64+
if (
65+
!setupCtx ||
66+
!t.isTSTypeReference(setupCtx) ||
67+
!t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
68+
)
69+
return;
70+
71+
const emitType = setupCtx.typeParameters?.params[0];
72+
if (!emitType) return;
73+
74+
ctx!.emitsTypeDecl = emitType;
75+
const runtimeEmits = extractRuntimeEmits(ctx!);
76+
77+
const ast = t.arrayExpression(
78+
Array.from(runtimeEmits).map((e) => t.stringLiteral(e))
79+
);
80+
options.properties.push(t.objectProperty(t.identifier('emits'), ast));
81+
}
82+
83+
return {
84+
name: 'babel-plugin-resolve-type',
85+
inherits: typescript,
86+
pre(file) {
87+
const filename = file.opts.filename || 'unknown.js';
88+
helpers = new Set();
89+
ctx = {
90+
filename: filename,
91+
source: file.code,
92+
options: this.compileOptions || {},
93+
ast: file.ast.program.body as any,
94+
error(msg, node) {
95+
throw new Error(
96+
`[@vue/babel-plugin-resolve-type] ${msg}\n\n${filename}\n${codeFrameColumns(
97+
file.code,
98+
{
99+
start: {
100+
line: node.loc!.start.line,
101+
column: node.loc!.start.column + 1,
102+
},
103+
end: {
104+
line: node.loc!.end.line,
105+
column: node.loc!.end.column + 1,
106+
},
107+
}
108+
)}`
109+
);
110+
},
111+
helper(key) {
112+
helpers!.add(key);
113+
return `_${key}`;
114+
},
115+
getString(node) {
116+
return file.code.slice(node.start!, node.end!);
117+
},
118+
bindingMetadata: Object.create(null),
119+
propsTypeDecl: undefined,
120+
propsRuntimeDefaults: undefined,
121+
propsDestructuredBindings: {},
122+
emitsTypeDecl: undefined,
123+
};
124+
},
125+
visitor: {
126+
CallExpression(path) {
127+
if (!ctx) {
128+
throw new Error(
129+
'[@vue/babel-plugin-resolve-type] context is not loaded.'
130+
);
131+
}
132+
133+
const node = path.node;
134+
if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return;
135+
136+
const comp = node.arguments[0];
137+
if (!comp || !t.isFunction(comp)) return;
138+
139+
let options = node.arguments[1];
140+
if (!options) {
141+
options = t.objectExpression([]);
142+
node.arguments.push(options);
143+
}
144+
145+
if (!t.isObjectExpression(options)) {
146+
throw new Error(
147+
'[@vue/babel-plugin-resolve-type] Options inside of defineComponent should be an object expression.'
148+
);
149+
}
150+
151+
processProps(comp, options);
152+
processEmits(comp, options);
153+
},
154+
},
155+
post(file) {
156+
for (const helper of helpers!) {
157+
addNamed(file.path, `_${helper}`, 'vue');
158+
}
159+
},
160+
};
161+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`resolve type > runtime emits > basic 1`] = `
4+
"import { type SetupContext, defineComponent } from 'vue';
5+
const Comp = defineComponent((props, {
6+
emit
7+
}: SetupContext<{
8+
change(val: string): void;
9+
click(): void;
10+
}>) => {
11+
emit('change');
12+
return () => {};
13+
}, {
14+
emits: [\\"change\\", \\"click\\"]
15+
});"
16+
`;
17+
18+
exports[`resolve type > runtime props > basic 1`] = `
19+
"import { defineComponent, h } from 'vue';
20+
interface Props {
21+
msg: string;
22+
optional?: boolean;
23+
}
24+
interface Props2 {
25+
set: Set<string>;
26+
}
27+
defineComponent((props: Props & Props2) => {
28+
return () => h('div', props.msg);
29+
}, {
30+
props: {
31+
msg: {
32+
type: String,
33+
required: true
34+
},
35+
optional: {
36+
type: Boolean,
37+
required: false
38+
},
39+
set: {
40+
type: Set,
41+
required: true
42+
}
43+
}
44+
});"
45+
`;
46+
47+
exports[`resolve type > runtime props > with dynamic default value 1`] = `
48+
"import { _mergeDefaults } from \\"vue\\";
49+
import { defineComponent, h } from 'vue';
50+
const defaults = {};
51+
defineComponent((props: {
52+
msg?: string;
53+
} = defaults) => {
54+
return () => h('div', props.msg);
55+
}, {
56+
props: _mergeDefaults({
57+
msg: {
58+
type: String,
59+
required: false
60+
}
61+
}, defaults)
62+
});"
63+
`;
64+
65+
exports[`resolve type > runtime props > with static default value 1`] = `
66+
"import { defineComponent, h } from 'vue';
67+
defineComponent((props: {
68+
msg?: string;
69+
} = {
70+
msg: 'hello'
71+
}) => {
72+
return () => h('div', props.msg);
73+
}, {
74+
props: {
75+
msg: {
76+
type: String,
77+
required: false,
78+
default: 'hello'
79+
}
80+
}
81+
});"
82+
`;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { transformAsync } from '@babel/core';
2+
import ResolveType from '../src';
3+
4+
async function transform(code: string): Promise<string> {
5+
const result = await transformAsync(code, { plugins: [ResolveType] });
6+
return result!.code!;
7+
}
8+
9+
describe('resolve type', () => {
10+
describe('runtime props', () => {
11+
test('basic', async () => {
12+
const result = await transform(
13+
`
14+
import { defineComponent, h } from 'vue';
15+
interface Props {
16+
msg: string;
17+
optional?: boolean;
18+
}
19+
interface Props2 {
20+
set: Set<string>;
21+
}
22+
defineComponent((props: Props & Props2) => {
23+
return () => h('div', props.msg);
24+
})
25+
`
26+
);
27+
expect(result).toMatchSnapshot();
28+
});
29+
30+
test('with static default value', async () => {
31+
const result = await transform(
32+
`
33+
import { defineComponent, h } from 'vue';
34+
defineComponent((props: { msg?: string } = { msg: 'hello' }) => {
35+
return () => h('div', props.msg);
36+
})
37+
`
38+
);
39+
expect(result).toMatchSnapshot();
40+
});
41+
42+
test('with dynamic default value', async () => {
43+
const result = await transform(
44+
`
45+
import { defineComponent, h } from 'vue';
46+
const defaults = {}
47+
defineComponent((props: { msg?: string } = defaults) => {
48+
return () => h('div', props.msg);
49+
})
50+
`
51+
);
52+
expect(result).toMatchSnapshot();
53+
});
54+
});
55+
56+
describe('runtime emits', () => {
57+
test('basic', async () => {
58+
const result = await transform(
59+
`
60+
import { type SetupContext, defineComponent } from 'vue';
61+
const Comp = defineComponent(
62+
(
63+
props,
64+
{ emit }: SetupContext<{ change(val: string): void; click(): void }>
65+
) => {
66+
emit('change');
67+
return () => {};
68+
}
69+
);
70+
`
71+
);
72+
expect(result).toMatchSnapshot();
73+
});
74+
});
75+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'tsup';
2+
3+
export default defineConfig({
4+
entry: ['src/index.ts'],
5+
format: ['cjs', 'esm'],
6+
dts: true,
7+
target: 'node14',
8+
platform: 'neutral',
9+
});

0 commit comments

Comments
 (0)