Skip to content
This repository was archived by the owner on Dec 25, 2024. It is now read-only.

Commit dd46b82

Browse files
committed
feat: withDefaults macro
1 parent 826e688 commit dd46b82

File tree

10 files changed

+273
-238
lines changed

10 files changed

+273
-238
lines changed

playground/App.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
<template>
22
<div>
3-
<hello-world msg="Hi"/>
3+
<hello-world name="Vue 2" @update="onUpdate" />
44
</div>
55
</template>
66

77
<script setup lang="ts">
88
import HelloWorld from './HelloWorld.vue'
9+
10+
function onUpdate(e: any) {
11+
console.log(e)
12+
}
913
</script>

playground/HelloWorld.vue

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div>
3-
<h3>{{msg}}</h3>
3+
<h3>{{ msg }}, {{ name }}</h3>
44
<button @click="inc">
55
Inc
66
</button>
@@ -10,14 +10,9 @@
1010
</template>
1111

1212
<script setup lang="ts">
13-
import { ref, computed } from '@vue/composition-api'
14-
15-
const props = defineProps({
16-
msg: {
17-
type: String,
18-
},
19-
})
13+
import { ref, computed, watch } from '@vue/composition-api'
2014
15+
const props = withDefaults(defineProps<{ msg: string; name: string | number }>(), { msg: 'Hello' })
2116
const emit = defineEmits()
2217
2318
const count = ref(0)
@@ -32,6 +27,8 @@ function dec() {
3227
}
3328
3429
const decText = '<b>Dec</b>'
30+
31+
watch(count, value => emit('update', value))
3532
</script>
3633

3734
<script lang="ts">

src/macros.ts

Lines changed: 156 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1+
// modified from https://github.com/vuejs/vue-next/blob/main/packages/compiler-sfc/src/compileScript.ts
2+
13
import {
24
Node,
3-
Declaration,
4-
ObjectPattern,
55
ObjectExpression,
6-
ArrayPattern,
7-
Identifier,
8-
ExportSpecifier,
9-
Function as FunctionNode,
106
TSType,
117
TSTypeLiteral,
128
TSFunctionType,
139
ObjectProperty,
14-
ArrayExpression,
1510
Statement,
1611
CallExpression,
17-
RestElement,
1812
TSInterfaceBody,
19-
AwaitExpression,
20-
VariableDeclarator,
21-
VariableDeclaration,
2213
} from '@babel/types'
2314
import { types as t } from '@babel/core'
15+
import { parseExpression } from '@babel/parser'
2416
import { PropTypeData } from './types'
2517

2618
// Special compiler macros
@@ -32,20 +24,22 @@ const WITH_DEFAULTS = 'withDefaults'
3224
export function applyMacros(nodes: Statement[]) {
3325
let hasDefinePropsCall = false
3426
let hasDefineEmitCall = false
35-
const hasDefineExposeCall = false
3627
let propsRuntimeDecl: Node | undefined
3728
let propsRuntimeDefaults: Node | undefined
3829
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
3930
let propsTypeDeclRaw: Node | undefined
40-
let propsIdentifier: string | undefined
4131
let emitsRuntimeDecl: Node | undefined
4232
let emitsTypeDecl:
4333
| TSFunctionType
4434
| TSTypeLiteral
4535
| TSInterfaceBody
4636
| undefined
4737
let emitsTypeDeclRaw: Node | undefined
48-
let emitIdentifier: string | undefined
38+
39+
// props/emits declared via types
40+
const typeDeclaredProps: Record<string, PropTypeData> = {}
41+
// record declared types for runtime props type generation
42+
const declaredTypes: Record<string, string[]> = {}
4943

5044
function error(
5145
msg: string,
@@ -196,58 +190,55 @@ export function applyMacros(nodes: Statement[]) {
196190
return false
197191
}
198192

199-
/* function genRuntimeProps(props: Record<string, PropTypeData>) {
193+
function genRuntimeProps(props: Record<string, PropTypeData>) {
200194
const keys = Object.keys(props)
201195
if (!keys.length)
202-
return ''
196+
return undefined
203197

204198
// check defaults. If the default object is an object literal with only
205199
// static properties, we can directly generate more optimzied default
206200
// decalrations. Otherwise we will have to fallback to runtime merging.
207-
const hasStaticDefaults
208-
= propsRuntimeDefaults
201+
const hasStaticDefaults = propsRuntimeDefaults
209202
&& propsRuntimeDefaults.type === 'ObjectExpression'
210203
&& propsRuntimeDefaults.properties.every(
211204
node => node.type === 'ObjectProperty' && !node.computed,
212205
)
213206

214-
let propsDecls = `{
215-
${keys
216-
.map((key) => {
217-
let defaultString: string | undefined
218-
if (hasStaticDefaults) {
219-
const prop = (
220-
propsRuntimeDefaults as ObjectExpression
221-
).properties.find(
222-
(node: any) => node.key.name === key,
223-
) as ObjectProperty
224-
if (prop) {
225-
// prop has corresponding static default value
226-
defaultString = `default: ${source.slice(
227-
prop.value.start! + startOffset,
228-
prop.value.end! + startOffset,
229-
)}`
230-
}
231-
}
207+
return t.objectExpression(
208+
Object.entries(props).map(([key, value]) => {
209+
const prop = hasStaticDefaults
210+
? (propsRuntimeDefaults as ObjectExpression).properties.find((node: any) => node.key.name === key) as ObjectProperty
211+
: undefined
232212

233-
const { type, required } = props[key]
234-
return `${key}: { type: ${toRuntimeTypeString(
235-
type,
236-
)}, required: ${required}${
237-
defaultString ? `, ${defaultString}` : ''
238-
} }`
239-
})
240-
.join(',\n ')}\n }`
213+
if (prop)
214+
value.required = false
241215

242-
if (propsRuntimeDefaults && !hasStaticDefaults) {
243-
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
244-
propsRuntimeDefaults.start! + startOffset,
245-
propsRuntimeDefaults.end! + startOffset,
246-
)})`
247-
}
216+
const entries = Object.entries(value).map(([key, value]) =>
217+
key === 'type'
218+
? t.objectProperty(t.identifier(key), t.arrayExpression(value.map((i: any) => t.identifier(i))) as any)
219+
: t.objectProperty(t.identifier(key), parseExpression(JSON.stringify(value)) as any),
220+
)
221+
222+
if (prop)
223+
entries.push(t.objectProperty(t.identifier('default'), prop.value as any))
248224

249-
return `\n props: ${propsDecls} as unknown as undefined,`
250-
} */
225+
return t.objectProperty(
226+
t.identifier(key),
227+
t.objectExpression(entries),
228+
)
229+
}),
230+
)
231+
}
232+
233+
function getProps() {
234+
if (propsRuntimeDecl)
235+
return propsRuntimeDecl
236+
237+
if (propsTypeDecl) {
238+
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
239+
return genRuntimeProps(typeDeclaredProps)
240+
}
241+
}
251242

252243
nodes = nodes
253244
.map((node) => {
@@ -273,7 +264,7 @@ export function applyMacros(nodes: Statement[]) {
273264

274265
return {
275266
nodes,
276-
props: propsRuntimeDecl,
267+
props: getProps(),
277268
}
278269
}
279270

@@ -290,3 +281,115 @@ function isCallOf(
290281
: test(node.callee.name))
291282
)
292283
}
284+
285+
function extractRuntimeProps(
286+
node: TSTypeLiteral | TSInterfaceBody,
287+
props: Record<string, PropTypeData>,
288+
declaredTypes: Record<string, string[]>,
289+
) {
290+
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
291+
for (const m of members) {
292+
if (
293+
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature')
294+
&& m.key.type === 'Identifier'
295+
) {
296+
let type
297+
if (m.type === 'TSMethodSignature') {
298+
type = ['Function']
299+
}
300+
else if (m.typeAnnotation) {
301+
type = inferRuntimeType(
302+
m.typeAnnotation.typeAnnotation,
303+
declaredTypes,
304+
)
305+
}
306+
props[m.key.name] = {
307+
key: m.key.name,
308+
required: !m.optional,
309+
type: type || ['null'],
310+
}
311+
}
312+
}
313+
}
314+
315+
function inferRuntimeType(
316+
node: TSType,
317+
declaredTypes: Record<string, string[]>,
318+
): string[] {
319+
switch (node.type) {
320+
case 'TSStringKeyword':
321+
return ['String']
322+
case 'TSNumberKeyword':
323+
return ['Number']
324+
case 'TSBooleanKeyword':
325+
return ['Boolean']
326+
case 'TSObjectKeyword':
327+
return ['Object']
328+
case 'TSTypeLiteral':
329+
// TODO (nice to have) generate runtime property validation
330+
return ['Object']
331+
case 'TSFunctionType':
332+
return ['Function']
333+
case 'TSArrayType':
334+
case 'TSTupleType':
335+
// TODO (nice to have) generate runtime element type/length checks
336+
return ['Array']
337+
338+
case 'TSLiteralType':
339+
switch (node.literal.type) {
340+
case 'StringLiteral':
341+
return ['String']
342+
case 'BooleanLiteral':
343+
return ['Boolean']
344+
case 'NumericLiteral':
345+
case 'BigIntLiteral':
346+
return ['Number']
347+
default:
348+
return ['null']
349+
}
350+
351+
case 'TSTypeReference':
352+
if (node.typeName.type === 'Identifier') {
353+
if (declaredTypes[node.typeName.name])
354+
return declaredTypes[node.typeName.name]
355+
356+
switch (node.typeName.name) {
357+
case 'Array':
358+
case 'Function':
359+
case 'Object':
360+
case 'Set':
361+
case 'Map':
362+
case 'WeakSet':
363+
case 'WeakMap':
364+
return [node.typeName.name]
365+
case 'Record':
366+
case 'Partial':
367+
case 'Readonly':
368+
case 'Pick':
369+
case 'Omit':
370+
case 'Exclude':
371+
case 'Extract':
372+
case 'Required':
373+
case 'InstanceType':
374+
return ['Object']
375+
}
376+
}
377+
return ['null']
378+
379+
case 'TSParenthesizedType':
380+
return inferRuntimeType(node.typeAnnotation, declaredTypes)
381+
case 'TSUnionType':
382+
return [
383+
...new Set(
384+
[].concat(
385+
...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any),
386+
),
387+
),
388+
]
389+
case 'TSIntersectionType':
390+
return ['Object']
391+
392+
default:
393+
return ['null'] // no runtime check
394+
}
395+
}

test/__snapshots__/parse.test.ts.snap

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)