|
|
@@ -36,8 +36,11 @@ import { rewriteDefault } from './rewriteDefault'
|
|
|
|
|
|
const DEFINE_PROPS = 'defineProps'
|
|
|
const DEFINE_EMIT = 'defineEmit'
|
|
|
-const DEFINE_EMITS = 'defineEmits'
|
|
|
const DEFINE_EXPOSE = 'defineExpose'
|
|
|
+const WITH_DEFAULTS = 'withDefaults'
|
|
|
+
|
|
|
+// deprecated
|
|
|
+const DEFINE_EMITS = 'defineEmits'
|
|
|
|
|
|
export interface SFCScriptCompileOptions {
|
|
|
/**
|
|
|
@@ -191,6 +194,7 @@ export function compileScript(
|
|
|
let hasDefineEmitCall = false
|
|
|
let hasDefineExposeCall = false
|
|
|
let propsRuntimeDecl: Node | undefined
|
|
|
+ let propsRuntimeDefaults: Node | undefined
|
|
|
let propsTypeDecl: TSTypeLiteral | undefined
|
|
|
let propsIdentifier: string | undefined
|
|
|
let emitRuntimeDecl: Node | undefined
|
|
|
@@ -262,68 +266,95 @@ export function compileScript(
|
|
|
}
|
|
|
|
|
|
function processDefineProps(node: Node): boolean {
|
|
|
- if (isCallOf(node, DEFINE_PROPS)) {
|
|
|
- if (hasDefinePropsCall) {
|
|
|
- error(`duplicate ${DEFINE_PROPS}() call`, node)
|
|
|
+ if (!isCallOf(node, DEFINE_PROPS)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasDefinePropsCall) {
|
|
|
+ error(`duplicate ${DEFINE_PROPS}() call`, node)
|
|
|
+ }
|
|
|
+ hasDefinePropsCall = true
|
|
|
+
|
|
|
+ propsRuntimeDecl = node.arguments[0]
|
|
|
+
|
|
|
+ // call has type parameters - infer runtime types from it
|
|
|
+ if (node.typeParameters) {
|
|
|
+ if (propsRuntimeDecl) {
|
|
|
+ error(
|
|
|
+ `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
|
|
|
+ `at the same time. Use one or the other.`,
|
|
|
+ node
|
|
|
+ )
|
|
|
}
|
|
|
- hasDefinePropsCall = true
|
|
|
- propsRuntimeDecl = node.arguments[0]
|
|
|
- // context call has type parameters - infer runtime types from it
|
|
|
- if (node.typeParameters) {
|
|
|
- if (propsRuntimeDecl) {
|
|
|
- error(
|
|
|
- `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
|
|
|
- `at the same time. Use one or the other.`,
|
|
|
- node
|
|
|
- )
|
|
|
- }
|
|
|
- const typeArg = node.typeParameters.params[0]
|
|
|
- if (typeArg.type === 'TSTypeLiteral') {
|
|
|
- propsTypeDecl = typeArg
|
|
|
- } else {
|
|
|
- error(
|
|
|
- `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
|
|
|
- typeArg
|
|
|
- )
|
|
|
- }
|
|
|
+
|
|
|
+ const typeArg = node.typeParameters.params[0]
|
|
|
+ if (typeArg.type === 'TSTypeLiteral') {
|
|
|
+ propsTypeDecl = typeArg
|
|
|
+ } else {
|
|
|
+ error(
|
|
|
+ `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
|
|
|
+ typeArg
|
|
|
+ )
|
|
|
}
|
|
|
- return true
|
|
|
}
|
|
|
- return false
|
|
|
+
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ function processWithDefaults(node: Node): boolean {
|
|
|
+ if (!isCallOf(node, WITH_DEFAULTS)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (processDefineProps(node.arguments[0])) {
|
|
|
+ if (propsRuntimeDecl) {
|
|
|
+ error(
|
|
|
+ `${WITH_DEFAULTS} can only be used with type-based ` +
|
|
|
+ `${DEFINE_PROPS} declaration.`,
|
|
|
+ node
|
|
|
+ )
|
|
|
+ }
|
|
|
+ propsRuntimeDefaults = node.arguments[1]
|
|
|
+ } else {
|
|
|
+ error(
|
|
|
+ `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
|
|
|
+ node.arguments[0] || node
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return true
|
|
|
}
|
|
|
|
|
|
function processDefineEmits(node: Node): boolean {
|
|
|
- if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) {
|
|
|
- if (hasDefineEmitCall) {
|
|
|
- error(`duplicate ${DEFINE_EMITS}() call`, node)
|
|
|
+ if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (hasDefineEmitCall) {
|
|
|
+ error(`duplicate ${DEFINE_EMITS}() call`, node)
|
|
|
+ }
|
|
|
+ hasDefineEmitCall = true
|
|
|
+ emitRuntimeDecl = node.arguments[0]
|
|
|
+ if (node.typeParameters) {
|
|
|
+ if (emitRuntimeDecl) {
|
|
|
+ error(
|
|
|
+ `${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
|
|
|
+ `at the same time. Use one or the other.`,
|
|
|
+ node
|
|
|
+ )
|
|
|
}
|
|
|
- hasDefineEmitCall = true
|
|
|
- emitRuntimeDecl = node.arguments[0]
|
|
|
- if (node.typeParameters) {
|
|
|
- if (emitRuntimeDecl) {
|
|
|
- error(
|
|
|
- `${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
|
|
|
- `at the same time. Use one or the other.`,
|
|
|
- node
|
|
|
- )
|
|
|
- }
|
|
|
- const typeArg = node.typeParameters.params[0]
|
|
|
- if (
|
|
|
- typeArg.type === 'TSFunctionType' ||
|
|
|
- typeArg.type === 'TSTypeLiteral'
|
|
|
- ) {
|
|
|
- emitTypeDecl = typeArg
|
|
|
- } else {
|
|
|
- error(
|
|
|
- `type argument passed to ${DEFINE_EMITS}() must be a function type ` +
|
|
|
- `or a literal type with call signatures.`,
|
|
|
- typeArg
|
|
|
- )
|
|
|
- }
|
|
|
+ const typeArg = node.typeParameters.params[0]
|
|
|
+ if (
|
|
|
+ typeArg.type === 'TSFunctionType' ||
|
|
|
+ typeArg.type === 'TSTypeLiteral'
|
|
|
+ ) {
|
|
|
+ emitTypeDecl = typeArg
|
|
|
+ } else {
|
|
|
+ error(
|
|
|
+ `type argument passed to ${DEFINE_EMITS}() must be a function type ` +
|
|
|
+ `or a literal type with call signatures.`,
|
|
|
+ typeArg
|
|
|
+ )
|
|
|
}
|
|
|
- return true
|
|
|
}
|
|
|
- return false
|
|
|
+ return true
|
|
|
}
|
|
|
|
|
|
function processDefineExpose(node: Node): boolean {
|
|
|
@@ -480,6 +511,63 @@ export function compileScript(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function genRuntimeProps(props: Record<string, PropTypeData>) {
|
|
|
+ const keys = Object.keys(props)
|
|
|
+ if (!keys.length) {
|
|
|
+ return ``
|
|
|
+ }
|
|
|
+
|
|
|
+ // check defaults. If the default object is an object literal with only
|
|
|
+ // static properties, we can directly generate more optimzied default
|
|
|
+ // decalrations. Otherwise we will have to fallback to runtime merging.
|
|
|
+ const hasStaticDefaults =
|
|
|
+ propsRuntimeDefaults &&
|
|
|
+ propsRuntimeDefaults.type === 'ObjectExpression' &&
|
|
|
+ propsRuntimeDefaults.properties.every(
|
|
|
+ node => node.type === 'ObjectProperty' && !node.computed
|
|
|
+ )
|
|
|
+
|
|
|
+ let propsDecls = `{
|
|
|
+ ${keys
|
|
|
+ .map(key => {
|
|
|
+ let defaultString: string | undefined
|
|
|
+ if (hasStaticDefaults) {
|
|
|
+ const prop = (propsRuntimeDefaults as ObjectExpression).properties.find(
|
|
|
+ (node: any) => node.key.name === key
|
|
|
+ ) as ObjectProperty
|
|
|
+ if (prop) {
|
|
|
+ // prop has corresponding static default value
|
|
|
+ defaultString = `default: ${source.slice(
|
|
|
+ prop.value.start! + startOffset,
|
|
|
+ prop.value.end! + startOffset
|
|
|
+ )}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (__DEV__) {
|
|
|
+ const { type, required } = props[key]
|
|
|
+ return `${key}: { type: ${toRuntimeTypeString(
|
|
|
+ type
|
|
|
+ )}, required: ${required}${
|
|
|
+ defaultString ? `, ${defaultString}` : ``
|
|
|
+ } }`
|
|
|
+ } else {
|
|
|
+ // production: checks are useless
|
|
|
+ return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .join(',\n ')}\n }`
|
|
|
+
|
|
|
+ if (propsRuntimeDefaults && !hasStaticDefaults) {
|
|
|
+ propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
|
|
|
+ propsRuntimeDefaults.start! + startOffset,
|
|
|
+ propsRuntimeDefaults.end! + startOffset
|
|
|
+ )})`
|
|
|
+ }
|
|
|
+
|
|
|
+ return `\n props: ${propsDecls} as unknown as undefined,`
|
|
|
+ }
|
|
|
+
|
|
|
// 1. process normal <script> first if it exists
|
|
|
let scriptAst
|
|
|
if (script) {
|
|
|
@@ -675,7 +763,8 @@ export function compileScript(
|
|
|
// process `defineProps` and `defineEmit(s)` calls
|
|
|
if (
|
|
|
processDefineProps(node.expression) ||
|
|
|
- processDefineEmits(node.expression)
|
|
|
+ processDefineEmits(node.expression) ||
|
|
|
+ processWithDefaults(node.expression)
|
|
|
) {
|
|
|
s.remove(node.start! + startOffset, node.end! + startOffset)
|
|
|
} else if (processDefineExpose(node.expression)) {
|
|
|
@@ -692,7 +781,8 @@ export function compileScript(
|
|
|
if (node.type === 'VariableDeclaration' && !node.declare) {
|
|
|
for (const decl of node.declarations) {
|
|
|
if (decl.init) {
|
|
|
- const isDefineProps = processDefineProps(decl.init)
|
|
|
+ const isDefineProps =
|
|
|
+ processDefineProps(decl.init) || processWithDefaults(decl.init)
|
|
|
if (isDefineProps) {
|
|
|
propsIdentifier = scriptSetup.content.slice(
|
|
|
decl.id.start!,
|
|
|
@@ -812,6 +902,7 @@ export function compileScript(
|
|
|
// 5. check useOptions args to make sure it doesn't reference setup scope
|
|
|
// variables
|
|
|
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
|
|
|
+ checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
|
|
|
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
|
|
|
|
|
|
// 6. remove non-script content
|
|
|
@@ -1080,9 +1171,14 @@ function walkDeclaration(
|
|
|
for (const { id, init } of node.declarations) {
|
|
|
const isDefineCall = !!(
|
|
|
isConst &&
|
|
|
- (isCallOf(init, DEFINE_PROPS) ||
|
|
|
- isCallOf(init, DEFINE_EMIT) ||
|
|
|
- isCallOf(init, DEFINE_EMITS))
|
|
|
+ isCallOf(
|
|
|
+ init,
|
|
|
+ c =>
|
|
|
+ c === DEFINE_PROPS ||
|
|
|
+ c === DEFINE_EMIT ||
|
|
|
+ c === DEFINE_EMITS ||
|
|
|
+ c === WITH_DEFAULTS
|
|
|
+ )
|
|
|
)
|
|
|
if (id.type === 'Identifier') {
|
|
|
let bindingType
|
|
|
@@ -1318,29 +1414,6 @@ function inferRuntimeType(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-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`
|
|
|
@@ -1567,13 +1640,15 @@ function isFunction(node: Node): node is FunctionNode {
|
|
|
|
|
|
function isCallOf(
|
|
|
node: Node | null | undefined,
|
|
|
- name: string
|
|
|
+ test: string | ((id: string) => boolean)
|
|
|
): node is CallExpression {
|
|
|
return !!(
|
|
|
node &&
|
|
|
node.type === 'CallExpression' &&
|
|
|
node.callee.type === 'Identifier' &&
|
|
|
- node.callee.name === name
|
|
|
+ (typeof test === 'string'
|
|
|
+ ? node.callee.name === test
|
|
|
+ : test(node.callee.name))
|
|
|
)
|
|
|
}
|
|
|
|