feat: resolve type

This commit is contained in:
三咲智子 Kevin Deng 2023-08-27 23:47:23 +08:00
parent 4050dd6da4
commit a8d9d1253a
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
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',
});

View File

@ -114,6 +114,37 @@ importers:
specifier: ^3.3.4
version: 3.3.4
packages/babel-plugin-resolve-type:
dependencies:
'@babel/code-frame':
specifier: ^7.22.10
version: 7.22.13
'@babel/helper-module-imports':
specifier: ^7.22.5
version: 7.22.15
'@babel/parser':
specifier: ^7.22.11
version: 7.22.16
'@babel/plugin-syntax-typescript':
specifier: ^7.22.5
version: 7.22.5(@babel/core@7.22.9)
'@vue/compiler-sfc':
specifier: link:/Users/kevin/Developer/open-source/vue/vue-core/packages/compiler-sfc
version: link:../../../vue-core/packages/compiler-sfc
devDependencies:
'@babel/core':
specifier: ^7.22.9
version: 7.22.9
'@types/babel__code-frame':
specifier: ^7.0.3
version: 7.0.3
'@types/babel__helper-module-imports':
specifier: ^7.18.0
version: 7.18.0
vue:
specifier: ^3.3.4
version: 3.3.4
packages/jsx-explorer:
dependencies:
'@babel/core':
@ -185,6 +216,28 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/core@7.22.9:
resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.1
'@babel/code-frame': 7.22.13
'@babel/generator': 7.22.9
'@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9)
'@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.9)
'@babel/helpers': 7.22.6
'@babel/parser': 7.22.16
'@babel/template': 7.22.15
'@babel/traverse': 7.22.20
'@babel/types': 7.22.19
convert-source-map: 1.9.0
debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
/@babel/generator@7.22.15:
resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==}
engines: {node: '>=6.9.0'}
@ -194,6 +247,15 @@ packages:
'@jridgewell/trace-mapping': 0.3.19
jsesc: 2.5.2
/@babel/generator@7.22.9:
resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.22.19
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.19
jsesc: 2.5.2
/@babel/helper-annotate-as-pure@7.22.5:
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
engines: {node: '>=6.9.0'}
@ -218,6 +280,19 @@ packages:
lru-cache: 5.1.1
semver: 6.3.1
/@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9):
resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.22.9
'@babel/core': 7.22.9
'@babel/helper-validator-option': 7.22.15
browserslist: 4.21.10
lru-cache: 5.1.1
semver: 6.3.1
/@babel/helper-create-class-features-plugin@7.22.11(@babel/core@7.22.20):
resolution: {integrity: sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==}
engines: {node: '>=6.9.0'}
@ -270,7 +345,6 @@ packages:
/@babel/helper-environment-visitor@7.22.5:
resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-function-name@7.22.5:
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
@ -325,6 +399,19 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
/@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9):
resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-environment-visitor': 7.22.5
'@babel/helper-module-imports': 7.22.15
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
/@babel/helper-optimise-call-expression@7.22.5:
resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
engines: {node: '>=6.9.0'}
@ -414,6 +501,16 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/helpers@7.22.6:
resolution: {integrity: sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.22.15
'@babel/traverse': 7.22.20
'@babel/types': 7.22.19
transitivePeerDependencies:
- supports-color
/@babel/highlight@7.22.13:
resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==}
engines: {node: '>=6.9.0'}
@ -635,6 +732,16 @@ packages:
'@babel/helper-plugin-utils': 7.22.5
dev: true
/@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.22.9):
resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-plugin-utils': 7.22.5
dev: false
/@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.22.20):
resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
engines: {node: '>=6.9.0'}
@ -1711,6 +1818,20 @@ packages:
engines: {node: '>= 10'}
dev: true
/@types/babel__code-frame@7.0.3:
resolution: {integrity: sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==}
dev: true
/@types/babel__core@7.20.1:
resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==}
dependencies:
'@babel/parser': 7.22.16
'@babel/types': 7.22.19
'@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.2
'@types/babel__traverse': 7.20.2
dev: true
/@types/babel__core@7.20.2:
resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==}
dependencies:
@ -1727,6 +1848,13 @@ packages:
'@babel/types': 7.22.19
dev: true
/@types/babel__helper-module-imports@7.18.0:
resolution: {integrity: sha512-bXrjmO0EhInafHtFIaimws2rDf8Sp0E6T3cstzSL4lUAPtzZ2GhoV48U6n4IHyBIBsd88r4JIw2UPTqlyWwXcg==}
dependencies:
'@types/babel__core': 7.20.1
'@types/babel__traverse': 7.20.2
dev: true
/@types/babel__template@7.4.2:
resolution: {integrity: sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==}
dependencies: