Skip to content

Commit d7b31d6

Browse files
committed
feat(macros): automatically infer type from default value
1 parent 1041236 commit d7b31d6

File tree

6 files changed

+307
-116
lines changed

6 files changed

+307
-116
lines changed

packages/macros/src/core/define-component/index.ts

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { importHelperFn, type MagicStringAST } from '@vue-macros/common'
22
import { walkIdentifiers } from '@vue/compiler-sfc'
3-
import { restructure } from '../restructure'
4-
import type { FunctionalNode, RootMapValue } from '..'
3+
import { isFunctionalNode, type FunctionalNode, type RootMapValue } from '..'
4+
import { getDefaultValue, restructure } from '../restructure'
55
import { transformAwait } from './await'
66
import { transformReturn } from './return'
7-
import type { ObjectExpression } from '@babel/types'
7+
import type { Node, ObjectExpression } from '@babel/types'
88

99
export function transformDefineComponent(
1010
root: FunctionalNode,
@@ -28,8 +28,40 @@ export function transformDefineComponent(
2828
if (root.params[0]) {
2929
if (root.params[0].type === 'Identifier') {
3030
getWalkedIds(root, propsName).forEach((id) => (props[id] = null))
31-
} else {
32-
const restructuredProps = restructure(s, root, {
31+
} else if (root.params[0].type === 'ObjectPattern') {
32+
const restructuredProps = root.params[0]
33+
for (const prop of restructuredProps.properties) {
34+
if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier')
35+
continue
36+
const propName = prop.key.name
37+
if (prop.value.type !== 'AssignmentPattern') {
38+
props[propName] = null
39+
continue
40+
}
41+
const defaultValue = getDefaultValue(prop.value.right)
42+
const isRequired = prop.value.right.type === 'TSNonNullExpression'
43+
44+
props[propName] = `{`
45+
if (isRequired) {
46+
props[propName] += 'required: true,'
47+
}
48+
if (defaultValue) {
49+
const { value, type, skipFactory } = getTypeAndValue(s, defaultValue)
50+
if (type) {
51+
props[propName] += `type: ${type},`
52+
}
53+
if (value) {
54+
props[propName] += `default: ${value},`
55+
}
56+
if (skipFactory) {
57+
props[propName] += 'skipFactory: true,'
58+
}
59+
}
60+
props[propName] += `}`
61+
}
62+
63+
restructure(s, root, {
64+
skipDefaultProps: true,
3365
generateRestProps: (restPropsName, index, list) => {
3466
if (index === list.length - 1) {
3567
hasRestProp = true
@@ -38,11 +70,6 @@ export function transformDefineComponent(
3870
}
3971
},
4072
})
41-
for (const prop of restructuredProps) {
42-
if (prop.path.endsWith('props') && !prop.isRest) {
43-
props[prop.name] = prop.isRequired ? '{ required: true }' : null
44-
}
45-
}
4673
}
4774
}
4875

@@ -79,16 +106,16 @@ export function transformDefineComponent(
79106

80107
const propsString = Object.entries(props)
81108
.map(([key, value]) => `'${key}': ${value}`)
82-
.join(', ')
109+
.join(', \n')
83110
if (propsString) {
84111
const argument = map.defineComponent.arguments[1]
85112
if (!argument) {
86113
s.appendRight(
87114
root.end!,
88-
`, {${hasRestProp ? 'inheritAttrs: false,' : ''} props: { ${propsString} } }`,
115+
`, {${hasRestProp ? 'inheritAttrs: false,' : ''} props: {\n${propsString}\n} }`,
89116
)
90117
} else if (argument.type === 'ObjectExpression') {
91-
prependObjectExpression(argument, 'props', `{ ${propsString} }`, s)
118+
prependObjectExpression(argument, 'props', `{\n${propsString}\n}`, s)
92119
if (hasRestProp) {
93120
prependObjectExpression(argument, 'inheritAttrs', 'false', s)
94121
}
@@ -121,24 +148,55 @@ function prependObjectExpression(
121148

122149
function getWalkedIds(root: FunctionalNode, propsName: string) {
123150
const walkedIds = new Set<string>()
124-
walkIdentifiers(
125-
root.body,
126-
(id, parent) => {
127-
if (
128-
id.name === propsName &&
129-
(parent?.type === 'MemberExpression' ||
130-
parent?.type === 'OptionalMemberExpression')
131-
) {
132-
const prop =
133-
parent.property.type === 'Identifier'
134-
? parent.property.name
135-
: parent.property.type === 'StringLiteral'
136-
? parent.property.value
137-
: ''
138-
if (prop) walkedIds.add(prop)
139-
}
140-
},
141-
false,
142-
)
151+
walkIdentifiers(root.body, (id, parent) => {
152+
if (
153+
id.name === propsName &&
154+
(parent?.type === 'MemberExpression' ||
155+
parent?.type === 'OptionalMemberExpression')
156+
) {
157+
const prop =
158+
parent.property.type === 'Identifier'
159+
? parent.property.name
160+
: parent.property.type === 'StringLiteral'
161+
? parent.property.value
162+
: ''
163+
if (prop) walkedIds.add(prop)
164+
}
165+
})
143166
return walkedIds
144167
}
168+
169+
function getTypeAndValue(s: MagicStringAST, node: Node) {
170+
let value = ''
171+
let type = ''
172+
let skipFactory = false
173+
if (node.type === 'StringLiteral') {
174+
type = 'String'
175+
value = `'${node.value}'`
176+
} else if (node.type === 'BooleanLiteral') {
177+
type = 'Boolean'
178+
value = String(node.value)
179+
} else if (node.type === 'NumericLiteral') {
180+
type = 'Number'
181+
value = String(node.value)
182+
} else if (node.type === 'ObjectExpression') {
183+
type = 'Object'
184+
value = `() => (${s.sliceNode(node)})`
185+
} else if (node.type === 'ArrayExpression') {
186+
type = 'Array'
187+
value = `() => (${s.sliceNode(node)})`
188+
} else if (isFunctionalNode(node)) {
189+
type = 'Function'
190+
value = s.sliceNode(node)
191+
} else if (node.type === 'Identifier') {
192+
if (node.name === 'undefined') {
193+
value = 'undefined'
194+
} else {
195+
skipFactory = true
196+
value = s.sliceNode(node)
197+
}
198+
} else if (node.type === 'NullLiteral') {
199+
value = 'null'
200+
}
201+
return { value, type, skipFactory }
202+
}

packages/macros/src/core/restructure.ts

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
HELPER_PREFIX,
33
importHelperFn,
4-
type MagicString,
4+
type MagicStringAST,
55
} from '@vue-macros/common'
66
import { walkIdentifiers } from '@vue/compiler-sfc'
77
import { withDefaultsHelperId } from './helper'
@@ -10,6 +10,7 @@ import type { Node } from '@babel/types'
1010

1111
type Options = {
1212
withDefaultsFrom?: string
13+
skipDefaultProps?: boolean
1314
generateRestProps?: (
1415
restPropsName: string,
1516
index: number,
@@ -23,45 +24,52 @@ type Prop = {
2324
value: string
2425
defaultValue?: string
2526
isRest?: boolean
26-
isRequired?: boolean
2727
}
2828

2929
export function restructure(
30-
s: MagicString,
30+
s: MagicStringAST,
3131
node: FunctionalNode,
3232
options: Options = {},
3333
): Prop[] {
3434
let index = 0
3535
const propList: Prop[] = []
3636
for (const param of node.params) {
3737
const path = `${HELPER_PREFIX}props${index++ || ''}`
38-
const props = getProps(param, path, s, [], options)
38+
const props = getProps(s, options, param, path)
3939
if (props) {
40-
const hasDefaultValue = props.some((i) => i.defaultValue)
4140
s.overwrite(param.start!, param.end!, path)
42-
propList.push(
43-
...(hasDefaultValue
44-
? props.map((i) => ({
45-
...i,
46-
path: i.path.replace(HELPER_PREFIX, `${HELPER_PREFIX}default_`),
47-
}))
48-
: props),
49-
)
41+
propList.push(...props)
5042
}
5143
}
5244

5345
if (propList.length) {
5446
const defaultValues: Record<string, Prop[]> = {}
5547
const rests = []
5648
for (const prop of propList) {
57-
if (prop.defaultValue) {
58-
const basePath = prop.path.split(/\.|\[/)[0]
59-
;(defaultValues[basePath] ??= []).push(prop)
60-
}
6149
if (prop.isRest) {
6250
rests.push(prop)
6351
}
52+
if (prop.defaultValue) {
53+
const paths = prop.path.split(/\.|\[/)
54+
if (!options.skipDefaultProps || paths.length !== 1) {
55+
;(defaultValues[paths[0]] ??= []).push(prop)
56+
}
57+
}
58+
}
59+
60+
for (const [index, rest] of rests.entries()) {
61+
prependFunctionalNode(
62+
node,
63+
s,
64+
options.generateRestProps?.(rest.name, index, rests) ??
65+
`\nconst ${rest.name} = ${importHelperFn(
66+
s,
67+
0,
68+
'createPropsRestProxy',
69+
)}(${rest.path}, [${rest.value}])`,
70+
)
6471
}
72+
6573
for (const [path, values] of Object.entries(defaultValues)) {
6674
const createPropsDefaultProxy = importHelperFn(
6775
s,
@@ -70,10 +78,6 @@ export function restructure(
7078
undefined,
7179
options.withDefaultsFrom ?? withDefaultsHelperId,
7280
)
73-
const resolvedPath = path.replace(
74-
`${HELPER_PREFIX}default_`,
75-
HELPER_PREFIX,
76-
)
7781
const resolvedValues = values
7882
.map(
7983
(i) => `'${i.path.replace(path, '')}${i.value}': ${i.defaultValue}`,
@@ -82,20 +86,7 @@ export function restructure(
8286
prependFunctionalNode(
8387
node,
8488
s,
85-
`\nconst ${path} = ${createPropsDefaultProxy}(${resolvedPath}, {${resolvedValues}})`,
86-
)
87-
}
88-
89-
for (const [index, rest] of rests.entries()) {
90-
prependFunctionalNode(
91-
node,
92-
s,
93-
options.generateRestProps?.(rest.name, index, rests) ??
94-
`\nconst ${rest.name} = ${importHelperFn(
95-
s,
96-
0,
97-
'createPropsRestProxy',
98-
)}(${rest.path}, [${rest.value}])`,
89+
`\n${path} = ${createPropsDefaultProxy}(${path}, {${resolvedValues}})`,
9990
)
10091
}
10192

@@ -123,11 +114,11 @@ export function restructure(
123114
}
124115

125116
function getProps(
117+
s: MagicStringAST,
118+
options: Options,
126119
node: Node,
127-
path: string = '',
128-
s: MagicString,
120+
path = '',
129121
props: Prop[] = [],
130-
options: Options,
131122
) {
132123
const properties =
133124
node.type === 'ObjectPattern'
@@ -141,7 +132,11 @@ function getProps(
141132
properties.forEach((prop, index) => {
142133
if (prop?.type === 'Identifier') {
143134
// { foo }
144-
props.push({ name: prop.name, path, value: `[${index}]` })
135+
props.push({
136+
name: prop.name,
137+
path,
138+
value: `[${index}]`,
139+
})
145140
propNames.push(`'${prop.name}'`)
146141
} else if (
147142
prop?.type === 'AssignmentPattern' &&
@@ -152,33 +147,41 @@ function getProps(
152147
path,
153148
name: prop.left.name,
154149
value: `[${index}]`,
155-
defaultValue: s.slice(prop.right.start!, prop.right.end!),
150+
defaultValue: s.sliceNode(getDefaultValue(prop.right)),
156151
})
157152
propNames.push(`'${prop.left.name}'`)
158153
} else if (
159154
prop?.type === 'ObjectProperty' &&
160155
prop.key.type === 'Identifier'
161156
) {
162-
if (
163-
prop.value.type === 'AssignmentPattern' &&
164-
prop.value.left.type === 'Identifier'
165-
) {
166-
// { foo: bar = 'foo' }
167-
props.push({
168-
path,
169-
name: prop.value.left.name,
170-
value: `.${prop.key.name}`,
171-
defaultValue: s.slice(prop.value.right.start!, prop.value.right.end!),
172-
isRequired: prop.value.right.type === 'TSNonNullExpression',
173-
})
157+
if (prop.value.type === 'AssignmentPattern') {
158+
if (prop.value.left.type === 'Identifier') {
159+
// { foo: bar = 'foo' }
160+
props.push({
161+
path,
162+
name: prop.value.left.name,
163+
value: `.${prop.key.name}`,
164+
defaultValue: s.sliceNode(getDefaultValue(prop.value.right)),
165+
})
166+
} else {
167+
// { foo: { bar } = {} }
168+
getProps(
169+
s,
170+
options,
171+
prop.value.left,
172+
`${path}.${prop.key.name}`,
173+
props,
174+
)
175+
}
174176
} else if (
175-
!getProps(prop.value, `${path}.${prop.key.name}`, s, props, options)
177+
!getProps(s, options, prop.value, `${path}.${prop.key.name}`, props)
176178
) {
177179
// { foo: bar }
180+
const name =
181+
prop.value.type === 'Identifier' ? prop.value.name : prop.key.name
178182
props.push({
179183
path,
180-
name:
181-
prop.value.type === 'Identifier' ? prop.value.name : prop.key.name,
184+
name,
182185
value: `.${prop.key.name}`,
183186
})
184187
}
@@ -196,15 +199,25 @@ function getProps(
196199
isRest: true,
197200
})
198201
} else if (prop) {
199-
getProps(prop, `${path}[${index}]`, s, props, options)
202+
getProps(s, options, prop, `${path}[${index}]`, props)
200203
}
201204
})
202205
return props.length ? props : undefined
203206
}
204207

208+
export function getDefaultValue(node: Node): Node {
209+
if (node.type === 'TSNonNullExpression') {
210+
return getDefaultValue(node.expression)
211+
}
212+
if (node.type === 'TSAsExpression') {
213+
return getDefaultValue(node.expression)
214+
}
215+
return node
216+
}
217+
205218
function prependFunctionalNode(
206219
node: FunctionalNode,
207-
s: MagicString,
220+
s: MagicStringAST,
208221
result: string,
209222
): void {
210223
const isBlockStatement = node.body.type === 'BlockStatement'

0 commit comments

Comments
 (0)