Program,
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
- ImportSpecifier
+ ImportSpecifier,
+ CallExpression
} from '@babel/types'
import { walk } from 'estree-walker'
return node
}
}
+
+export function isCallOf(
+ node: Node | null | undefined,
+ test: string | ((id: string) => boolean) | null | undefined
+): node is CallExpression {
+ return !!(
+ node &&
+ test &&
+ node.type === 'CallExpression' &&
+ node.callee.type === 'Identifier' &&
+ (typeof test === 'string'
+ ? node.callee.name === test
+ : test(node.callee.name))
+ )
+}
--- /dev/null
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`sfc props transform > aliasing 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+
+ let x = foo
+ let y = __props.foo
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props.foo + __props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform > basic usage 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+
+ console.log(__props.foo)
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform > computed static key 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+
+ console.log(__props.foo)
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform > default values w/ array runtime declaration 1`] = `
+"import { mergeDefaults as _mergeDefaults } from 'vue'
+
+export default {
+ props: _mergeDefaults(['foo', 'bar', 'baz'], {
+ foo: 1,
+ bar: () => ({}),
+ func: () => {}, __skip_func: true
+}),
+ setup(__props) {
+
+
+
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform > default values w/ object runtime declaration 1`] = `
+"import { mergeDefaults as _mergeDefaults } from 'vue'
+
+export default {
+ props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
+ foo: 1,
+ bar: () => ({}),
+ func: () => {}, __skip_func: true,
+ ext: x, __skip_ext: true
+}),
+ setup(__props) {
+
+
+
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform > default values w/ type declaration 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ foo: { type: Number, required: false, default: 1 },
+ bar: { type: Object, required: false, default: () => ({}) },
+ func: { type: Function, required: false, default: () => {} }
+ },
+ setup(__props: any) {
+
+
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform > default values w/ type declaration, prod mode 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ foo: { default: 1 },
+ bar: { default: () => ({}) },
+ baz: null,
+ boola: { type: Boolean },
+ boolb: { type: [Boolean, Number] },
+ func: { type: Function, default: () => {} }
+ },
+ setup(__props: any) {
+
+
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform > multiple variable declarations 1`] = `
+"import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+ const bar = 'fish', hello = 'world'
+
+return (_ctx, _cache) => {
+ return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.foo) + \\" \\" + _toDisplayString(hello) + \\" \\" + _toDisplayString(bar), 1 /* TEXT */))
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform > nested scope 1`] = `
+"export default {
+ props: ['foo', 'bar'],
+ setup(__props) {
+
+
+ function test(foo) {
+ console.log(foo)
+ console.log(__props.bar)
+ }
+
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform > non-identifier prop names 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: { 'foo.bar': Function },
+ setup(__props) {
+
+
+ let x = __props[\\"foo.bar\\"]
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props[\\"foo.bar\\"])
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform > rest spread 1`] = `
+"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
+
+export default {
+ props: ['foo', 'bar', 'baz'],
+ setup(__props) {
+
+const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"]);
+
+
+
+return () => {}
+}
+
+}"
+`;
function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
return compileSFCScript(src, {
inlineTemplate: true,
- reactivityTransform: true,
...options
})
}
})
})
- test('$$() escape', () => {
- const { content } = compile(`
- <script setup>
- const { foo, bar: baz } = defineProps(['foo'])
- console.log($$(foo))
- console.log($$(baz))
- $$({ foo, baz })
- </script>
- `)
- expect(content).toMatch(`const __props_foo = _toRef(__props, 'foo')`)
- expect(content).toMatch(`const __props_bar = _toRef(__props, 'bar')`)
- expect(content).toMatch(`console.log((__props_foo))`)
- expect(content).toMatch(`console.log((__props_bar))`)
- expect(content).toMatch(`({ foo: __props_foo, baz: __props_bar })`)
- assertCode(content)
- })
-
// #6960
test('computed static key', () => {
const { content, bindings } = compile(`
).toThrow(`cannot reference locally declared variables`)
})
- test('should error if assignment to constant variable', () => {
+ test('should error if assignment to destructured prop binding', () => {
expect(() =>
compile(
`<script setup>
foo = 'bar'
</script>`
)
- ).toThrow(`Assignment to constant variable.`)
+ ).toThrow(`Cannot assign to destructured props`)
+
+ expect(() =>
+ compile(
+ `<script setup>
+ let { foo } = defineProps(['foo'])
+ foo = 'bar'
+ </script>`
+ )
+ ).toThrow(`Cannot assign to destructured props`)
+ })
+
+ test('should error when watching destructured prop', () => {
+ expect(() =>
+ compile(
+ `<script setup>
+ import { watch } from 'vue'
+ const { foo } = defineProps(['foo'])
+ watch(foo, () => {})
+ </script>`
+ )
+ ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
+
+ expect(() =>
+ compile(
+ `<script setup>
+ import { watch as w } from 'vue'
+ const { foo } = defineProps(['foo'])
+ w(foo, () => {})
+ </script>`
+ )
+ ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
+ })
+
+ // not comprehensive, but should help for most common cases
+ test('should error if default value type does not match declared type', () => {
+ expect(() =>
+ compile(
+ `<script setup lang="ts">
+ const { foo = 'hello' } = defineProps<{ foo?: number }>()
+ </script>`
+ )
+ ).toThrow(`Default value of prop "foo" does not match declared type.`)
})
})
})
isFunctionType,
walkIdentifiers,
getImportedName,
- unwrapTSNode
+ unwrapTSNode,
+ isCallOf
} from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import {
import { rewriteDefaultAST } from './rewriteDefault'
import { createCache } from './cache'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
+import { transformDestructuredProps } from './compileScriptPropsDestructure'
// Special compiler macros
const DEFINE_PROPS = 'defineProps'
isUsedInTemplate: boolean
}
+export type PropsDestructureBindings = Record<
+ string, // public prop key
+ {
+ local: string // local identifier, may be different
+ default?: Expression
+ }
+>
+
type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
type EmitsDeclType = FromNormalScript<
// feature flags
// TODO remove support for deprecated options when out of experimental
const enableReactivityTransform = !!options.reactivityTransform
- const enablePropsTransform = !!options.reactivityTransform
const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false
const hoistStatic = options.hoistStatic !== false && !script
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
// props destructure data
- const propsDestructuredBindings: Record<
- string, // public prop key
- {
- local: string // local identifier, may be different
- default?: Expression
- isConst: boolean
- }
- > = Object.create(null)
+ const propsDestructuredBindings: PropsDestructureBindings =
+ Object.create(null)
// magic-string state
const s = new MagicString(source)
}
}
- function processDefineProps(
- node: Node,
- declId?: LVal,
- declKind?: VariableDeclaration['kind']
- ): boolean {
+ function processDefineProps(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_PROPS)) {
return false
}
}
if (declId) {
- const isConst = declKind === 'const'
- if (enablePropsTransform && declId.type === 'ObjectPattern') {
+ // handle props destructure
+ if (declId.type === 'ObjectPattern') {
propsDestructureDecl = declId
- // props destructure - handle compilation sugar
for (const prop of declId.properties) {
if (prop.type === 'ObjectProperty') {
const propKey = resolveObjectKey(prop.key, prop.computed)
// store default value
propsDestructuredBindings[propKey] = {
local: left.name,
- default: right,
- isConst
+ default: right
}
} else if (prop.value.type === 'Identifier') {
// simple destructure
propsDestructuredBindings[propKey] = {
- local: prop.value.name,
- isConst
+ local: prop.value.name
}
} else {
error(
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
- if (processDefineProps(node.arguments[0], declId, declKind)) {
+ warnOnce(
+ `withDefaults() has been deprecated. ` +
+ `Props destructure is now reactive by default - ` +
+ `use destructure with default values instead.`
+ )
+ if (processDefineProps(node.arguments[0], declId)) {
if (propsRuntimeDecl) {
error(
`${WITH_DEFAULTS} can only be used with type-based ` +
defaultVal.start!,
defaultVal.end!
)
+
const unwrapped = unwrapTSNode(defaultVal)
+
+ if (
+ inferredType &&
+ inferredType.length &&
+ !inferredType.includes(UNKNOWN_TYPE)
+ ) {
+ const valueType = inferValueType(unwrapped)
+ if (valueType && !inferredType.includes(valueType)) {
+ error(
+ `Default value of prop "${key}" does not match declared type.`,
+ unwrapped
+ )
+ }
+ }
+
// If the default value is a function or is an identifier referencing
// external value, skip factory wrap. This is needed when using
// destructure w/ runtime declaration since we cannot safely infer
const needSkipFactory =
!inferredType &&
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
+
const needFactoryWrap =
!needSkipFactory &&
!isLiteralNode(unwrapped) &&
!inferredType?.includes('Function')
+
return {
valueString: needFactoryWrap ? `() => (${value})` : value,
needSkipFactory
}
// apply reactivity transform
+ // TODO remove in 3.4
if (enableReactivityTransform && shouldTransform(script.content)) {
const { rootRefs, importedHelpers } = transformAST(
scriptAst,
// defineProps / defineEmits
const isDefineProps =
- processDefineProps(init, decl.id, node.kind) ||
+ processDefineProps(init, decl.id) ||
processWithDefaults(init, decl.id, node.kind)
const isDefineEmits = processDefineEmits(init, decl.id)
if (isDefineProps || isDefineEmits) {
}
}
- // 3. Apply reactivity transform
+ // 3.1 props destructure transform
+ if (propsDestructureDecl) {
+ transformDestructuredProps(
+ scriptSetupAst,
+ s,
+ startOffset,
+ propsDestructuredBindings,
+ error,
+ vueImportAliases.watch
+ )
+ }
+
+ // 3.2 Apply reactivity transform
+ // TODO remove in 3.4
if (
- (enableReactivityTransform &&
- // normal <script> had ref bindings that maybe used in <script setup>
- (refBindings || shouldTransform(scriptSetup.content))) ||
- propsDestructureDecl
+ enableReactivityTransform &&
+ // normal <script> had ref bindings that maybe used in <script setup>
+ (refBindings || shouldTransform(scriptSetup.content))
) {
const { rootRefs, importedHelpers } = transformAST(
scriptSetupAst,
s,
startOffset,
- refBindings,
- propsDestructuredBindings
+ refBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
}
- // 5. check useOptions args to make sure it doesn't reference setup scope
+ // 5. check macro args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
return types.size ? [...types] : ['Number']
}
+// non-comprehensive, best-effort type infernece for a runtime value
+// this is used to catch default value / type declaration mismatches
+// when using props destructure.
+function inferValueType(node: Node): string | undefined {
+ switch (node.type) {
+ case 'StringLiteral':
+ return 'String'
+ case 'NumericLiteral':
+ return 'Number'
+ case 'BooleanLiteral':
+ return 'Boolean'
+ case 'ObjectExpression':
+ return 'Object'
+ case 'ArrayExpression':
+ return 'Array'
+ case 'FunctionExpression':
+ case 'ArrowFunctionExpression':
+ return 'Function'
+ }
+}
+
function extractRuntimeEmits(
node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
emits: Set<string>
: ``
}
-function isCallOf(
- node: Node | null | undefined,
- test: string | ((id: string) => boolean) | null | undefined
-): node is CallExpression {
- return !!(
- node &&
- test &&
- node.type === 'CallExpression' &&
- node.callee.type === 'Identifier' &&
- (typeof test === 'string'
- ? node.callee.name === test
- : test(node.callee.name))
- )
-}
-
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
if (isCallOf(node, userReactiveImport)) {
return true
--- /dev/null
+import {
+ Node,
+ Identifier,
+ BlockStatement,
+ Program,
+ VariableDeclaration
+} from '@babel/types'
+import MagicString from 'magic-string'
+import { walk } from 'estree-walker'
+import {
+ extractIdentifiers,
+ isFunctionType,
+ isInDestructureAssignment,
+ isReferencedIdentifier,
+ isStaticProperty,
+ walkFunctionParams,
+ isCallOf,
+ unwrapTSNode
+} from '@vue/compiler-core'
+import { hasOwn, genPropsAccessExp } from '@vue/shared'
+import { PropsDestructureBindings } from './compileScript'
+
+/**
+ * true -> prop binding
+ * false -> local binding
+ */
+type Scope = Record<string, boolean>
+
+export function transformDestructuredProps(
+ ast: Program,
+ s: MagicString,
+ offset = 0,
+ knownProps: PropsDestructureBindings,
+ error: (msg: string, node: Node, end?: number) => never,
+ watchMethodName = 'watch'
+) {
+ const rootScope: Scope = {}
+ const scopeStack: Scope[] = [rootScope]
+ let currentScope: Scope = rootScope
+ const excludedIds = new WeakSet<Identifier>()
+ const parentStack: Node[] = []
+ const propsLocalToPublicMap: Record<string, string> = Object.create(null)
+
+ for (const key in knownProps) {
+ const { local } = knownProps[key]
+ rootScope[local] = true
+ propsLocalToPublicMap[local] = key
+ }
+
+ function registerLocalBinding(id: Identifier) {
+ excludedIds.add(id)
+ if (currentScope) {
+ currentScope[id.name] = false
+ } else {
+ error(
+ 'registerBinding called without active scope, something is wrong.',
+ id
+ )
+ }
+ }
+
+ function walkScope(node: Program | BlockStatement, isRoot = false) {
+ for (const stmt of node.body) {
+ if (stmt.type === 'VariableDeclaration') {
+ walkVariableDeclaration(stmt, isRoot)
+ } else if (
+ stmt.type === 'FunctionDeclaration' ||
+ stmt.type === 'ClassDeclaration'
+ ) {
+ if (stmt.declare || !stmt.id) continue
+ registerLocalBinding(stmt.id)
+ } else if (
+ (stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
+ stmt.left.type === 'VariableDeclaration'
+ ) {
+ walkVariableDeclaration(stmt.left)
+ } else if (
+ stmt.type === 'ExportNamedDeclaration' &&
+ stmt.declaration &&
+ stmt.declaration.type === 'VariableDeclaration'
+ ) {
+ walkVariableDeclaration(stmt.declaration, isRoot)
+ } else if (
+ stmt.type === 'LabeledStatement' &&
+ stmt.body.type === 'VariableDeclaration'
+ ) {
+ walkVariableDeclaration(stmt.body, isRoot)
+ }
+ }
+ }
+
+ function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
+ if (stmt.declare) {
+ return
+ }
+ for (const decl of stmt.declarations) {
+ const isDefineProps =
+ isRoot && decl.init && isCallOf(unwrapTSNode(decl.init), 'defineProps')
+ for (const id of extractIdentifiers(decl.id)) {
+ if (isDefineProps) {
+ // for defineProps destructure, only exclude them since they
+ // are already passed in as knownProps
+ excludedIds.add(id)
+ } else {
+ registerLocalBinding(id)
+ }
+ }
+ }
+ }
+
+ function rewriteId(
+ scope: Scope,
+ id: Identifier,
+ parent: Node,
+ parentStack: Node[]
+ ): boolean {
+ if (hasOwn(scope, id.name)) {
+ const binding = scope[id.name]
+
+ if (binding) {
+ if (
+ (parent.type === 'AssignmentExpression' && id === parent.left) ||
+ parent.type === 'UpdateExpression'
+ ) {
+ error(`Cannot assign to destructured props as they are readonly.`, id)
+ }
+
+ if (isStaticProperty(parent) && parent.shorthand) {
+ // let binding used in a property shorthand
+ // skip for destructure patterns
+ if (
+ !(parent as any).inPattern ||
+ isInDestructureAssignment(parent, parentStack)
+ ) {
+ // { prop } -> { prop: __props.prop }
+ s.appendLeft(
+ id.end! + offset,
+ `: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
+ )
+ }
+ } else {
+ // x --> __props.x
+ s.overwrite(
+ id.start! + offset,
+ id.end! + offset,
+ genPropsAccessExp(propsLocalToPublicMap[id.name])
+ )
+ }
+ }
+ return true
+ }
+ return false
+ }
+
+ // check root scope first
+ walkScope(ast, true)
+ ;(walk as any)(ast, {
+ enter(node: Node, parent?: Node) {
+ parent && parentStack.push(parent)
+
+ // skip type nodes
+ if (
+ parent &&
+ parent.type.startsWith('TS') &&
+ parent.type !== 'TSAsExpression' &&
+ parent.type !== 'TSNonNullExpression' &&
+ parent.type !== 'TSTypeAssertion'
+ ) {
+ return this.skip()
+ }
+
+ if (isCallOf(node, watchMethodName)) {
+ const arg = unwrapTSNode(node.arguments[0])
+ if (arg.type === 'Identifier') {
+ error(
+ `"${arg.name}" is a destructured prop and cannot be directly watched. ` +
+ `Use a getter () => ${arg.name} instead.`,
+ arg
+ )
+ }
+ }
+
+ // function scopes
+ if (isFunctionType(node)) {
+ scopeStack.push((currentScope = {}))
+ walkFunctionParams(node, registerLocalBinding)
+ if (node.body.type === 'BlockStatement') {
+ walkScope(node.body)
+ }
+ return
+ }
+
+ // catch param
+ if (node.type === 'CatchClause') {
+ scopeStack.push((currentScope = {}))
+ if (node.param && node.param.type === 'Identifier') {
+ registerLocalBinding(node.param)
+ }
+ walkScope(node.body)
+ return
+ }
+
+ // non-function block scopes
+ if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
+ scopeStack.push((currentScope = {}))
+ walkScope(node)
+ return
+ }
+
+ if (node.type === 'Identifier') {
+ if (
+ isReferencedIdentifier(node, parent!, parentStack) &&
+ !excludedIds.has(node)
+ ) {
+ // walk up the scope chain to check if id should be appended .value
+ let i = scopeStack.length
+ while (i--) {
+ if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
+ return
+ }
+ }
+ }
+ }
+ },
+ leave(node: Node, parent?: Node) {
+ parent && parentStack.pop()
+ if (
+ (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
+ isFunctionType(node)
+ ) {
+ scopeStack.pop()
+ currentScope = scopeStack[scopeStack.length - 1] || null
+ }
+ }
+ })
+}
*
* This is only usable inside `<script setup>`, is compiled away in the output
* and should **not** be actually called at runtime.
+ *
+ * @deprecated use reactive props destructure instead.
*/
export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
props: Props,