diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts
index 78ffd4e1625..a0658b4d75d 100644
--- a/packages/compiler-core/src/options.ts
+++ b/packages/compiler-core/src/options.ts
@@ -82,6 +82,11 @@ export const enum BindingTypes {
* declared as a prop
*/
PROPS = 'props',
+ /**
+ * a local alias of a `
+ {{ foo }}
+ `)
+ expect(content).not.toMatch(`const { foo } =`)
+ expect(content).toMatch(`console.log(__props.foo)`)
+ expect(content).toMatch(`_toDisplayString(__props.foo)`)
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ foo: BindingTypes.PROPS
+ })
+ })
+
+ test('nested scope', () => {
+ const { content, bindings } = compile(`
+
+ `)
+ expect(content).not.toMatch(`const { foo, bar } =`)
+ expect(content).toMatch(`console.log(foo)`)
+ expect(content).toMatch(`console.log(__props.bar)`)
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ foo: BindingTypes.PROPS,
+ bar: BindingTypes.PROPS,
+ test: BindingTypes.SETUP_CONST
+ })
+ })
+
+ test('default values w/ runtime declaration', () => {
+ const { content } = compile(`
+
+ `)
+ // literals can be used as-is, non-literals are always returned from a
+ // function
+ expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
+ foo: 1,
+ bar: () => {}
+})`)
+ assertCode(content)
+ })
+
+ test('default values w/ type declaration', () => {
+ const { content } = compile(`
+
+ `)
+ // literals can be used as-is, non-literals are always returned from a
+ // function
+ expect(content).toMatch(`props: {
+ foo: { type: Number, required: false, default: 1 },
+ bar: { type: Object, required: false, default: () => {} }
+ }`)
+ assertCode(content)
+ })
+
+ test('default values w/ type declaration, prod mode', () => {
+ const { content } = compile(
+ `
+
+ `,
+ { isProd: true }
+ )
+ // literals can be used as-is, non-literals are always returned from a
+ // function
+ expect(content).toMatch(`props: {
+ foo: { default: 1 },
+ bar: { default: () => {} },
+ baz: null
+ }`)
+ assertCode(content)
+ })
+
+ test('aliasing', () => {
+ const { content, bindings } = compile(`
+
+ {{ foo + bar }}
+ `)
+ expect(content).not.toMatch(`const { foo: bar } =`)
+ expect(content).toMatch(`let x = foo`) // should not process
+ expect(content).toMatch(`let y = __props.foo`)
+ // should convert bar to __props.foo in template expressions
+ expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`)
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ x: BindingTypes.SETUP_LET,
+ y: BindingTypes.SETUP_LET,
+ foo: BindingTypes.PROPS,
+ bar: BindingTypes.PROPS_ALIASED,
+ __propsAliases: {
+ bar: 'foo'
+ }
+ })
+ })
+
+ test('rest spread', () => {
+ const { content, bindings } = compile(`
+
+ `)
+ expect(content).toMatch(
+ `const rest = _createPropsRestProxy(__props, ["foo","bar"])`
+ )
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ foo: BindingTypes.PROPS,
+ bar: BindingTypes.PROPS,
+ baz: BindingTypes.PROPS,
+ rest: BindingTypes.SETUP_CONST
+ })
+ })
+
+ describe('errors', () => {
+ test('should error on deep destructure', () => {
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(`destructure does not support nested patterns`)
+
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(`destructure does not support nested patterns`)
+ })
+
+ test('should error on computed key', () => {
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(`destructure cannot use computed key`)
+ })
+
+ test('should error when used with withDefaults', () => {
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(`withDefaults() is unnecessary when using destructure`)
+ })
+
+ test('should error if destructure reference local vars', () => {
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(`cannot reference locally declared variables`)
+ })
+ })
+})
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 0dac40b2340..94d13f6471a 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -39,7 +39,9 @@ import {
TSInterfaceBody,
AwaitExpression,
Program,
- ObjectMethod
+ ObjectMethod,
+ LVal,
+ Expression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
@@ -88,9 +90,15 @@ export interface SFCScriptCompileOptions {
/**
* (Experimental) Enable syntax transform for using refs without `.value`
* https://github.com/vuejs/rfcs/discussions/369
- * @default true
+ * @default false
*/
refTransform?: boolean
+ /**
+ * (Experimental) Enable syntax transform for destructuring from defineProps()
+ * https://github.com/vuejs/rfcs/discussions/394
+ * @default false
+ */
+ propsDestructureTransform?: boolean
/**
* @deprecated use `refTransform` instead.
*/
@@ -131,6 +139,8 @@ export function compileScript(
let { script, scriptSetup, source, filename } = sfc
// feature flags
const enableRefTransform = !!options.refSugar || !!options.refTransform
+ const enablePropsTransform = !!options.propsDestructureTransform
+ const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false
let refBindings: string[] | undefined
@@ -203,7 +213,7 @@ export function compileScript(
cssVars,
bindings,
scopeId,
- !!options.isProd
+ isProd
)
content += `\nexport default __default__`
}
@@ -248,6 +258,8 @@ export function compileScript(
let hasDefineExposeCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: ObjectExpression | undefined
+ let propsDestructureDecl: Node | undefined
+ let propsDestructureRestId: string | undefined
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
let propsTypeDeclRaw: Node | undefined
let propsIdentifier: string | undefined
@@ -266,6 +278,14 @@ export function compileScript(
const typeDeclaredEmits: Set = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record = {}
+ // props destructure data
+ const propsDestructuredBindings: Record<
+ string, // public prop key
+ {
+ local: string // local identifier, may be different
+ default?: Expression
+ }
+ > = Object.create(null)
// magic-string state
const s = new MagicString(source)
@@ -337,7 +357,7 @@ export function compileScript(
}
}
- function processDefineProps(node: Node): boolean {
+ function processDefineProps(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_PROPS)) {
return false
}
@@ -374,14 +394,62 @@ export function compileScript(
}
}
+ if (declId) {
+ if (enablePropsTransform && declId.type === 'ObjectPattern') {
+ propsDestructureDecl = declId
+ // props destructure - handle compilation sugar
+ for (const prop of declId.properties) {
+ if (prop.type === 'ObjectProperty') {
+ if (prop.computed) {
+ error(
+ `${DEFINE_PROPS}() destructure cannot use computed key.`,
+ prop.key
+ )
+ }
+ const propKey = (prop.key as Identifier).name
+ if (prop.value.type === 'AssignmentPattern') {
+ // default value { foo = 123 }
+ const { left, right } = prop.value
+ if (left.type !== 'Identifier') {
+ error(
+ `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+ left
+ )
+ }
+ // store default value
+ propsDestructuredBindings[propKey] = {
+ local: left.name,
+ default: right
+ }
+ } else if (prop.value.type === 'Identifier') {
+ // simple destucture
+ propsDestructuredBindings[propKey] = {
+ local: prop.value.name
+ }
+ } else {
+ error(
+ `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+ prop.value
+ )
+ }
+ } else {
+ // rest spread
+ propsDestructureRestId = (prop.argument as Identifier).name
+ }
+ }
+ } else {
+ propsIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
+ }
+ }
+
return true
}
- function processWithDefaults(node: Node): boolean {
+ function processWithDefaults(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
- if (processDefineProps(node.arguments[0])) {
+ if (processDefineProps(node.arguments[0], declId)) {
if (propsRuntimeDecl) {
error(
`${WITH_DEFAULTS} can only be used with type-based ` +
@@ -389,6 +457,13 @@ export function compileScript(
node
)
}
+ if (propsDestructureDecl) {
+ error(
+ `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
+ `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
+ node.callee
+ )
+ }
propsRuntimeDefaults = node.arguments[1] as ObjectExpression
if (
!propsRuntimeDefaults ||
@@ -408,7 +483,7 @@ export function compileScript(
return true
}
- function processDefineEmits(node: Node): boolean {
+ function processDefineEmits(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_EMITS)) {
return false
}
@@ -440,6 +515,11 @@ export function compileScript(
)
}
}
+
+ if (declId) {
+ emitIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
+ }
+
return true
}
@@ -565,7 +645,7 @@ export function compileScript(
* static properties, we can directly generate more optimzied default
* declarations. Otherwise we will have to fallback to runtime merging.
*/
- function checkStaticDefaults() {
+ function hasStaticWithDefaults() {
return (
propsRuntimeDefaults &&
propsRuntimeDefaults.type === 'ObjectExpression' &&
@@ -582,13 +662,16 @@ export function compileScript(
if (!keys.length) {
return ``
}
- const hasStaticDefaults = checkStaticDefaults()
+ const hasStaticDefaults = hasStaticWithDefaults()
const scriptSetupSource = scriptSetup!.content
let propsDecls = `{
${keys
.map(key => {
let defaultString: string | undefined
- if (hasStaticDefaults) {
+ const destructured = genDestructuredDefaultValue(key)
+ if (destructured) {
+ defaultString = `default: ${destructured}`
+ } else if (hasStaticDefaults) {
const prop = propsRuntimeDefaults!.properties.find(
(node: any) => node.key.name === key
) as ObjectProperty | ObjectMethod
@@ -608,7 +691,7 @@ export function compileScript(
}
}
- if (__DEV__) {
+ if (!isProd) {
const { type, required } = props[key]
return `${key}: { type: ${toRuntimeTypeString(
type
@@ -632,9 +715,21 @@ export function compileScript(
return `\n props: ${propsDecls},`
}
+ function genDestructuredDefaultValue(key: string): string | undefined {
+ const destructured = propsDestructuredBindings[key]
+ if (destructured && destructured.default) {
+ const value = scriptSetup!.content.slice(
+ destructured.default.start!,
+ destructured.default.end!
+ )
+ const isLiteral = destructured.default.type.endsWith('Literal')
+ return isLiteral ? value : `() => ${value}`
+ }
+ }
+
function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) {
const scriptSetupSource = scriptSetup!.content
- if (checkStaticDefaults()) {
+ if (hasStaticWithDefaults()) {
// if withDefaults() is used, we need to remove the optional flags
// on props that have default values
let res = `{ `
@@ -754,7 +849,7 @@ export function compileScript(
// apply ref transform
if (enableRefTransform && shouldTransformRef(script.content)) {
- const { rootVars, importedHelpers } = transformRefAST(
+ const { rootRefs: rootVars, importedHelpers } = transformRefAST(
scriptAst,
s,
scriptStartOffset!
@@ -900,20 +995,9 @@ export function compileScript(
if (decl.init) {
// defineProps / defineEmits
const isDefineProps =
- processDefineProps(decl.init) || processWithDefaults(decl.init)
- if (isDefineProps) {
- propsIdentifier = scriptSetup.content.slice(
- decl.id.start!,
- decl.id.end!
- )
- }
- const isDefineEmits = processDefineEmits(decl.init)
- if (isDefineEmits) {
- emitIdentifier = scriptSetup.content.slice(
- decl.id.start!,
- decl.id.end!
- )
- }
+ processDefineProps(decl.init, decl.id) ||
+ processWithDefaults(decl.init, decl.id)
+ const isDefineEmits = processDefineEmits(decl.init, decl.id)
if (isDefineProps || isDefineEmits) {
if (left === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -1004,14 +1088,19 @@ export function compileScript(
}
// 3. Apply ref sugar transform
- if (enableRefTransform && shouldTransformRef(scriptSetup.content)) {
- const { rootVars, importedHelpers } = transformRefAST(
+ if (
+ (enableRefTransform && shouldTransformRef(scriptSetup.content)) ||
+ propsDestructureDecl
+ ) {
+ const { rootRefs, importedHelpers } = transformRefAST(
scriptSetupAst,
s,
startOffset,
- refBindings
+ refBindings,
+ propsDestructuredBindings,
+ !enableRefTransform
)
- refBindings = refBindings ? [...refBindings, ...rootVars] : rootVars
+ refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
helperImports.add(h)
}
@@ -1019,7 +1108,7 @@ export function compileScript(
// 4. extract runtime props/emits code from setup context type
if (propsTypeDecl) {
- extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
+ extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
}
if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
@@ -1029,6 +1118,7 @@ export function compileScript(
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
+ checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
// 6. remove non-script content
@@ -1062,6 +1152,20 @@ export function compileScript(
for (const key in typeDeclaredProps) {
bindingMetadata[key] = BindingTypes.PROPS
}
+ // props aliases
+ if (propsDestructureDecl) {
+ if (propsDestructureRestId) {
+ bindingMetadata[propsDestructureRestId] = BindingTypes.SETUP_CONST
+ }
+ for (const key in propsDestructuredBindings) {
+ const { local } = propsDestructuredBindings[key]
+ if (local !== key) {
+ bindingMetadata[local] = BindingTypes.PROPS_ALIASED
+ ;(bindingMetadata.__propsAliases ||
+ (bindingMetadata.__propsAliases = {}))[local] = key
+ }
+ }
+ }
for (const [key, { isType, imported, source }] of Object.entries(
userImports
)) {
@@ -1090,12 +1194,7 @@ export function compileScript(
helperImports.add('unref')
s.prependRight(
startOffset,
- `\n${genCssVarsCode(
- cssVars,
- bindingMetadata,
- scopeId,
- !!options.isProd
- )}\n`
+ `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
)
}
@@ -1118,6 +1217,14 @@ export function compileScript(
}`
)
}
+ if (propsDestructureRestId) {
+ s.prependRight(
+ startOffset,
+ `\nconst ${propsDestructureRestId} = ${helper(
+ `createPropsRestProxy`
+ )}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))})`
+ )
+ }
// inject temp variables for async context preservation
if (hasAwait) {
const any = isTS ? `: any` : ``
@@ -1235,9 +1342,22 @@ export function compileScript(
runtimeOptions += `\n __ssrInlineRender: true,`
}
if (propsRuntimeDecl) {
- runtimeOptions += `\n props: ${scriptSetup.content
+ let declCode = scriptSetup.content
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
- .trim()},`
+ .trim()
+ if (propsDestructureDecl) {
+ const defaults: string[] = []
+ for (const key in propsDestructuredBindings) {
+ const d = genDestructuredDefaultValue(key)
+ if (d) defaults.push(`${key}: ${d}`)
+ }
+ if (defaults.length) {
+ declCode = `${helper(
+ `mergeDefaults`
+ )}(${declCode}, {\n ${defaults.join(',\n ')}\n})`
+ }
+ }
+ runtimeOptions += `\n props: ${declCode},`
} else if (propsTypeDecl) {
runtimeOptions += genRuntimeProps(typeDeclaredProps)
}
@@ -1313,6 +1433,7 @@ export function compileScript(
}
s.trim()
+
return {
...scriptSetup,
bindings: bindingMetadata,
@@ -1376,10 +1497,16 @@ function walkDeclaration(
bindingType = BindingTypes.SETUP_LET
}
registerBinding(bindings, id, bindingType)
- } else if (id.type === 'ObjectPattern') {
- walkObjectPattern(id, bindings, isConst, isDefineCall)
- } else if (id.type === 'ArrayPattern') {
- walkArrayPattern(id, bindings, isConst, isDefineCall)
+ } else {
+ if (isCallOf(init, DEFINE_PROPS)) {
+ // skip walking props destructure
+ return
+ }
+ if (id.type === 'ObjectPattern') {
+ walkObjectPattern(id, bindings, isConst, isDefineCall)
+ } else if (id.type === 'ArrayPattern') {
+ walkArrayPattern(id, bindings, isConst, isDefineCall)
+ }
}
}
} else if (
@@ -1488,7 +1615,8 @@ function recordType(node: Node, declaredTypes: Record) {
function extractRuntimeProps(
node: TSTypeLiteral | TSInterfaceBody,
props: Record,
- declaredTypes: Record
+ declaredTypes: Record,
+ isProd: boolean
) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
@@ -1497,7 +1625,7 @@ function extractRuntimeProps(
m.key.type === 'Identifier'
) {
let type
- if (__DEV__) {
+ if (!isProd) {
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
diff --git a/packages/ref-transform/README.md b/packages/ref-transform/README.md
index e5ba22fb2bf..7de8d6d2146 100644
--- a/packages/ref-transform/README.md
+++ b/packages/ref-transform/README.md
@@ -64,7 +64,9 @@ const {
// @babel/parser plugins to enable.
// 'typescript' and 'jsx' will be auto-inferred from filename if provided,
// so in most cases explicit parserPlugins are not necessary
- parserPlugins: [/* ... */]
+ parserPlugins: [
+ /* ... */
+ ]
})
```
@@ -93,7 +95,7 @@ const ast = parse(src, { sourceType: 'module' })
const s = new MagicString(src)
const {
- rootVars, // ['a']
+ rootRefs, // ['a']
importedHelpers // ['ref']
} = transformAST(ast, s)
diff --git a/packages/ref-transform/__tests__/refTransform.spec.ts b/packages/ref-transform/__tests__/refTransform.spec.ts
index 1b9f3003bfb..37f55bb6ee9 100644
--- a/packages/ref-transform/__tests__/refTransform.spec.ts
+++ b/packages/ref-transform/__tests__/refTransform.spec.ts
@@ -17,7 +17,7 @@ function assertCode(code: string) {
}
test('$ unwrapping', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
import { ref, shallowRef } from 'vue'
let foo = $(ref())
let a = $(ref(1))
@@ -40,12 +40,12 @@ test('$ unwrapping', () => {
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
- expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+ expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
assertCode(code)
})
test('$ref & $shallowRef declarations', () => {
- const { code, rootVars, importedHelpers } = transform(`
+ const { code, rootRefs, importedHelpers } = transform(`
let foo = $ref()
let a = $ref(1)
let b = $shallowRef({
@@ -70,13 +70,13 @@ test('$ref & $shallowRef declarations', () => {
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
- expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
+ expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
assertCode(code)
})
test('multi $ref declarations', () => {
- const { code, rootVars, importedHelpers } = transform(`
+ const { code, rootRefs, importedHelpers } = transform(`
let a = $ref(1), b = $ref(2), c = $ref({
count: 0
})
@@ -86,31 +86,31 @@ test('multi $ref declarations', () => {
count: 0
})
`)
- expect(rootVars).toStrictEqual(['a', 'b', 'c'])
+ expect(rootRefs).toStrictEqual(['a', 'b', 'c'])
expect(importedHelpers).toStrictEqual(['ref'])
assertCode(code)
})
test('$computed declaration', () => {
- const { code, rootVars, importedHelpers } = transform(`
+ const { code, rootRefs, importedHelpers } = transform(`
let a = $computed(() => 1)
`)
expect(code).toMatch(`
let a = _computed(() => 1)
`)
- expect(rootVars).toStrictEqual(['a'])
+ expect(rootRefs).toStrictEqual(['a'])
expect(importedHelpers).toStrictEqual(['computed'])
assertCode(code)
})
test('mixing $ref & $computed declarations', () => {
- const { code, rootVars, importedHelpers } = transform(`
+ const { code, rootRefs, importedHelpers } = transform(`
let a = $ref(1), b = $computed(() => a + 1)
`)
expect(code).toMatch(`
let a = _ref(1), b = _computed(() => a.value + 1)
`)
- expect(rootVars).toStrictEqual(['a', 'b'])
+ expect(rootRefs).toStrictEqual(['a', 'b'])
expect(importedHelpers).toStrictEqual(['ref', 'computed'])
assertCode(code)
})
@@ -201,7 +201,7 @@ test('should not rewrite scope variable', () => {
})
test('object destructure', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo())
let { foo } = $(useSomthing(() => 1));
console.log(n, a, c, d, f, g, foo)
@@ -221,12 +221,12 @@ test('object destructure', () => {
expect(code).toMatch(
`console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
)
- expect(rootVars).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
+ expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
assertCode(code)
})
test('array destructure', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
console.log(n, a, b, c)
`)
@@ -235,12 +235,12 @@ test('array destructure', () => {
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
- expect(rootVars).toStrictEqual(['n', 'a', 'b', 'c'])
+ expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
assertCode(code)
})
test('nested destructure', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
let [{ a: { b }}] = $(useFoo())
let { c: [d, e] } = $(useBar())
console.log(b, d, e)
@@ -252,7 +252,7 @@ test('nested destructure', () => {
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
- expect(rootVars).toStrictEqual(['b', 'd', 'e'])
+ expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
assertCode(code)
})
@@ -270,7 +270,7 @@ test('$$', () => {
})
test('nested scopes', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
let a = $ref(0)
let b = $ref(0)
let c = 0
@@ -303,7 +303,7 @@ test('nested scopes', () => {
return $$({ a, b, c, d })
}
`)
- expect(rootVars).toStrictEqual(['a', 'b', 'bar'])
+ expect(rootRefs).toStrictEqual(['a', 'b', 'bar'])
expect(code).toMatch('a.value++ // outer a')
expect(code).toMatch('b.value++ // outer b')
diff --git a/packages/ref-transform/src/refTransform.ts b/packages/ref-transform/src/refTransform.ts
index d577347661c..4822a4e064c 100644
--- a/packages/ref-transform/src/refTransform.ts
+++ b/packages/ref-transform/src/refTransform.ts
@@ -31,7 +31,7 @@ export function shouldTransform(src: string): boolean {
return transformCheckRE.test(src)
}
-type Scope = Record
+type Scope = Record
export interface RefTransformOptions {
filename?: string
@@ -43,7 +43,7 @@ export interface RefTransformOptions {
export interface RefTransformResults {
code: string
map: SourceMap | null
- rootVars: string[]
+ rootRefs: string[]
importedHelpers: string[]
}
@@ -99,13 +99,23 @@ export function transformAST(
ast: Program,
s: MagicString,
offset = 0,
- knownRootVars?: string[]
+ knownRefs?: string[],
+ knownProps?: Record<
+ string, // public prop key
+ {
+ local: string // local identifier, may be different
+ default?: any
+ }
+ >,
+ rewritePropsOnly = false
): {
- rootVars: string[]
+ rootRefs: string[]
importedHelpers: string[]
} {
// TODO remove when out of experimental
- warnExperimental()
+ if (!rewritePropsOnly) {
+ warnExperimental()
+ }
const importedHelpers = new Set()
const rootScope: Scope = {}
@@ -113,14 +123,23 @@ export function transformAST(
let currentScope: Scope = rootScope
const excludedIds = new WeakSet()
const parentStack: Node[] = []
+ const propsLocalToPublicMap = Object.create(null)
- if (knownRootVars) {
- for (const key of knownRootVars) {
+ if (knownRefs) {
+ for (const key of knownRefs) {
rootScope[key] = true
}
}
+ if (knownProps) {
+ for (const key in knownProps) {
+ const { local } = knownProps[key]
+ rootScope[local] = 'prop'
+ propsLocalToPublicMap[local] = key
+ }
+ }
function error(msg: string, node: Node) {
+ if (rewritePropsOnly) return
const e = new Error(msg)
;(e as any).node = node
throw e
@@ -145,17 +164,19 @@ export function transformAST(
const registerRefBinding = (id: Identifier) => registerBinding(id, true)
- function walkScope(node: Program | BlockStatement) {
+ function walkScope(node: Program | BlockStatement, isRoot = false) {
for (const stmt of node.body) {
if (stmt.type === 'VariableDeclaration') {
if (stmt.declare) continue
for (const decl of stmt.declarations) {
let toVarCall
- if (
+ const isCall =
decl.init &&
decl.init.type === 'CallExpression' &&
- decl.init.callee.type === 'Identifier' &&
- (toVarCall = isToVarCall(decl.init.callee.name))
+ decl.init.callee.type === 'Identifier'
+ if (
+ isCall &&
+ (toVarCall = isToVarCall((decl as any).init.callee.name))
) {
processRefDeclaration(
toVarCall,
@@ -164,8 +185,18 @@ export function transformAST(
stmt
)
} else {
+ const isProps =
+ isRoot &&
+ isCall &&
+ (decl as any).init.callee.name === 'defineProps'
for (const id of extractIdentifiers(decl.id)) {
- registerBinding(id)
+ if (isProps) {
+ // for defineProps destructure, only exclude them since they
+ // are already passed in as knownProps
+ excludedIds.add(id)
+ } else {
+ registerBinding(id)
+ }
}
}
}
@@ -303,26 +334,48 @@ export function transformAST(
}
}
- function checkRefId(
+ function rewriteId(
scope: Scope,
id: Identifier,
parent: Node,
parentStack: Node[]
): boolean {
if (hasOwn(scope, id.name)) {
- if (scope[id.name]) {
+ const bindingType = scope[id.name]
+ if (bindingType) {
+ const isProp = bindingType === 'prop'
+ if (rewritePropsOnly && !isProp) {
+ return true
+ }
+ // ref
if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// { foo } -> { foo: foo.value }
+ // { prop } -> { prop: __prop.prop }
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
- s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+ if (isProp) {
+ s.appendLeft(
+ id.end! + offset,
+ `: __props.${propsLocalToPublicMap[id.name]}`
+ )
+ } else {
+ s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+ }
}
} else {
- s.appendLeft(id.end! + offset, '.value')
+ if (isProp) {
+ s.overwrite(
+ id.start! + offset,
+ id.end! + offset,
+ `__props.${propsLocalToPublicMap[id.name]}`
+ )
+ } else {
+ s.appendLeft(id.end! + offset, '.value')
+ }
}
}
return true
@@ -331,7 +384,7 @@ export function transformAST(
}
// check root scope first
- walkScope(ast)
+ walkScope(ast, true)
;(walk as any)(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)
@@ -371,7 +424,7 @@ export function transformAST(
// walk up the scope chain to check if id should be appended .value
let i = scopeStack.length
while (i--) {
- if (checkRefId(scopeStack[i], node, parent!, parentStack)) {
+ if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
return
}
}
@@ -424,7 +477,7 @@ export function transformAST(
})
return {
- rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
+ rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
importedHelpers: [...importedHelpers]
}
}
diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
index 728cd8e732a..e9dd1717bb2 100644
--- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
+++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
@@ -11,7 +11,8 @@ import {
SetupContext,
Suspense,
computed,
- ComputedRef
+ ComputedRef,
+ shallowReactive
} from '@vue/runtime-test'
import {
defineEmits,
@@ -21,7 +22,8 @@ import {
useAttrs,
useSlots,
mergeDefaults,
- withAsyncContext
+ withAsyncContext,
+ createPropsRestProxy
} from '../src/apiSetupHelpers'
describe('SFC
-
+