wip: generate runtime prop type checks in dev

This commit is contained in:
Evan You 2020-07-09 11:55:04 -04:00
parent 3e1cdba9db
commit 2c3cdab93d
2 changed files with 174 additions and 18 deletions

View File

@ -35,6 +35,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/parser": "^7.10.4", "@babel/parser": "^7.10.4",
"@babel/types": "^7.10.4",
"@vue/compiler-core": "3.0.0-beta.20", "@vue/compiler-core": "3.0.0-beta.20",
"@vue/compiler-dom": "3.0.0-beta.20", "@vue/compiler-dom": "3.0.0-beta.20",
"@vue/compiler-ssr": "3.0.0-beta.20", "@vue/compiler-ssr": "3.0.0-beta.20",

View File

@ -11,6 +11,7 @@ import {
ExpressionStatement, ExpressionStatement,
ArrowFunctionExpression, ArrowFunctionExpression,
ExportSpecifier, ExportSpecifier,
TSType,
TSTypeLiteral, TSTypeLiteral,
TSFunctionType, TSFunctionType,
TSDeclareFunction TSDeclareFunction
@ -25,6 +26,8 @@ export interface SFCScriptCompileOptions {
parserPlugins?: ParserPlugin[] parserPlugins?: ParserPlugin[]
} }
let hasWarned = false
/** /**
* Compile `<script setup>` * Compile `<script setup>`
* It requires the whole SFC descriptor because we need to handle and merge * It requires the whole SFC descriptor because we need to handle and merge
@ -34,6 +37,14 @@ export function compileScriptSetup(
sfc: SFCDescriptor, sfc: SFCDescriptor,
options: SFCScriptCompileOptions = {} options: SFCScriptCompileOptions = {}
) { ) {
if (__DEV__ && !__TEST__ && !hasWarned) {
hasWarned = true
console.log(
`\n[@vue/compiler-sfc] <script setup> is still an experimental proposal.\n` +
`Follow https://github.com/vuejs/rfcs/pull/182 for its status.\n`
)
}
const { script, scriptSetup, source, filename } = sfc const { script, scriptSetup, source, filename } = sfc
if (!scriptSetup) { if (!scriptSetup) {
throw new Error('SFC has no <script setup>.') throw new Error('SFC has no <script setup>.')
@ -159,8 +170,10 @@ export function compileScriptSetup(
let setupCtxASTNode let setupCtxASTNode
// props/emits declared via types // props/emits declared via types
const typeDeclaredProps: Set<string> = new Set() const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set() const typeDeclaredEmits: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
if (isTS && hasExplicitSignature) { if (isTS && hasExplicitSignature) {
// <script setup="xxx" lang="ts"> // <script setup="xxx" lang="ts">
@ -368,7 +381,7 @@ export function compileScriptSetup(
if (typeNode.type === 'TSTypeLiteral') { if (typeNode.type === 'TSTypeLiteral') {
if (id.name === propsVar) { if (id.name === propsVar) {
propsType = typeString propsType = typeString
extractProps(typeNode, typeDeclaredProps) extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
} else if (id.name === slotsVar) { } else if (id.name === slotsVar) {
slotsType = typeString slotsType = typeString
} else if (id.name === attrsVar) { } else if (id.name === attrsVar) {
@ -379,7 +392,7 @@ export function compileScriptSetup(
typeNode.type === 'TSFunctionType' typeNode.type === 'TSFunctionType'
) { ) {
emitType = typeString emitType = typeString
extractEmits(typeNode, typeDeclaredEmits) extractRuntimeEmits(typeNode, typeDeclaredEmits)
} }
} }
} }
@ -394,7 +407,7 @@ export function compileScriptSetup(
const index = node.id.start! + startOffset const index = node.id.start! + startOffset
s.overwrite(index, index + emitVar.length, '__emit__') s.overwrite(index, index + emitVar.length, '__emit__')
emitType = `typeof __emit__` emitType = `typeof __emit__`
extractEmits(node, typeDeclaredEmits) extractRuntimeEmits(node, typeDeclaredEmits)
} }
// move all type declarations to outer scope // move all type declarations to outer scope
@ -402,6 +415,7 @@ export function compileScriptSetup(
node.type.startsWith('TS') || node.type.startsWith('TS') ||
(node.type === 'ExportNamedDeclaration' && node.exportKind === 'type') (node.type === 'ExportNamedDeclaration' && node.exportKind === 'type')
) { ) {
recordType(node, declaredTypes)
s.move(start, end, 0) s.move(start, end, 0)
} }
} }
@ -493,16 +507,8 @@ export function compileScriptSetup(
// we have to use object spread for types to be merged properly // we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets // user's TS setting should compile it down to proper targets
const def = defaultExport ? `\n ...__default__,` : `` const def = defaultExport ? `\n ...__default__,` : ``
const runtimeProps = typeDeclaredProps.size const runtimeProps = genRuntimeProps(typeDeclaredProps)
? `\n props: [${Array.from(typeDeclaredProps) const runtimeEmits = genRuntimeEmits(typeDeclaredEmits)
.map(p => JSON.stringify(p))
.join(', ')}] as unknown as undefined,`
: ``
const runtimeEmits = typeDeclaredEmits.size
? `\n emits: [${Array.from(typeDeclaredEmits)
.map(p => JSON.stringify(p))
.join(', ')}] as unknown as undefined,`
: ``
s.append( s.append(
`export default __define__({${def}${runtimeProps}${runtimeEmits}\n setup\n})` `export default __define__({${def}${runtimeProps}${runtimeEmits}\n setup\n})`
) )
@ -608,16 +614,157 @@ function walkPattern(node: Node, bindings: Record<string, boolean>) {
} }
} }
function extractProps(node: TSTypeLiteral, props: Set<string>) { interface PropTypeData {
// TODO generate type/required checks in dev key: string
type: string[]
required: boolean
}
function recordType(node: Node, declaredTypes: Record<string, string[]>) {
if (node.type === 'TSInterfaceDeclaration') {
declaredTypes[node.id.name] = [`Object`]
} else if (node.type === 'TSTypeAliasDeclaration') {
declaredTypes[node.id.name] = inferRuntimeType(
node.typeAnnotation,
declaredTypes
)
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
recordType(node.declaration, declaredTypes)
}
}
function extractRuntimeProps(
node: TSTypeLiteral,
props: Record<string, PropTypeData>,
declaredTypes: Record<string, string[]>
) {
for (const m of node.members) { for (const m of node.members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') { if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
props.add(m.key.name) props[m.key.name] = {
key: m.key.name,
required: !m.optional,
type:
__DEV__ && m.typeAnnotation
? inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
: [`null`]
}
} }
} }
} }
function extractEmits( function inferRuntimeType(
node: TSType,
declaredTypes: Record<string, string[]>
): string[] {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSTypeLiteral':
// TODO (nice to have) generate runtime property validation
return ['Object']
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
// TODO (nice to have) genrate runtime element type/length checks
return ['Array']
case 'TSLiteralType':
switch (node.literal.type) {
case 'StringLiteral':
return ['String']
case 'BooleanLiteral':
return ['Boolean']
case 'NumericLiteral':
case 'BigIntLiteral':
return ['Number']
default:
return [`null`]
}
case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
if (declaredTypes[node.typeName.name]) {
return declaredTypes[node.typeName.name]
}
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
return [node.typeName.name]
case 'Record':
case 'Partial':
case 'Readonly':
case 'Pick':
case 'Omit':
case 'Exclude':
case 'Extract':
case 'Required':
case 'InstanceType':
return ['Object']
}
}
return [`null`]
case 'TSUnionType':
return [
...new Set(
[].concat(node.types.map(t =>
inferRuntimeType(t, declaredTypes)
) as any)
)
]
case 'TSIntersectionType':
return ['Object']
default:
return [`null`] // no runtime check
}
}
function genRuntimeProps(props: Record<string, PropTypeData>) {
const keys = Object.keys(props)
if (!keys.length) {
return ``
}
if (!__DEV__) {
// production: generate array version only
return `\n props: [\n ${keys
.map(k => JSON.stringify(k))
.join(',\n ')}\n ] as unknown as undefined,`
}
return `\n props: {\n ${keys
.map(key => {
const { type, required } = props[key]
return `${key}: { type: ${toRuntimeTypeString(
type
)}, required: ${required} }`
})
.join(',\n ')}\n } as unknown as undefined,`
}
function toRuntimeTypeString(types: string[]) {
return types.some(t => t === 'null')
? `null`
: types.length > 1
? `[${types.join(', ')}]`
: types[0]
}
function extractRuntimeEmits(
node: TSFunctionType | TSDeclareFunction, node: TSFunctionType | TSDeclareFunction,
emits: Set<string> emits: Set<string>
) { ) {
@ -641,6 +788,14 @@ function extractEmits(
} }
} }
function genRuntimeEmits(emits: Set<string>) {
return emits.size
? `\n emits: [${Array.from(emits)
.map(p => JSON.stringify(p))
.join(', ')}] as unknown as undefined,`
: ``
}
/** /**
* export default {} inside <script setup> cannot access variables declared * export default {} inside <script setup> cannot access variables declared
* inside since it's hoisted. Walk and check to make sure. * inside since it's hoisted. Walk and check to make sure.