Skip to content

Commit

Permalink
feat: resolve type
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Sep 25, 2023
1 parent 4050dd6 commit a8d9d12
Show file tree
Hide file tree
Showing 7 changed files with 498 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/babel-plugin-resolve-type/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# babel-plugin-resolve-type
41 changes: 41 additions & 0 deletions packages/babel-plugin-resolve-type/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
161 changes: 161 additions & 0 deletions packages/babel-plugin-resolve-type/src/index.ts
Original file line number Diff line number Diff line change
@@ -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');
}
},
};
};
Original file line number Diff line number Diff line change
@@ -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'
}
}
});"
`;
75 changes: 75 additions & 0 deletions packages/babel-plugin-resolve-type/test/resolve-type.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
9 changes: 9 additions & 0 deletions packages/babel-plugin-resolve-type/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
target: 'node14',
platform: 'neutral',
});
Loading

0 comments on commit a8d9d12

Please sign in to comment.