feat(reactivity-transform): use toRef() for $() destructure codegen

- now supports destructuring reactive objects
- no longer supports rest elements
This commit is contained in:
Evan You 2021-12-11 17:10:31 +08:00
parent 2db9c909c2
commit 93ba6b974e
4 changed files with 206 additions and 106 deletions

View File

@ -81,15 +81,22 @@ export interface SFCScriptCompileOptions {
* https://babeljs.io/docs/en/babel-parser#plugins * https://babeljs.io/docs/en/babel-parser#plugins
*/ */
babelParserPlugins?: ParserPlugin[] babelParserPlugins?: ParserPlugin[]
/**
* (Experimental) Enable syntax transform for using refs without `.value` and
* using destructured props with reactivity
*/
reactivityTransform?: boolean
/** /**
* (Experimental) Enable syntax transform for using refs without `.value` * (Experimental) Enable syntax transform for using refs without `.value`
* https://github.com/vuejs/rfcs/discussions/369 * https://github.com/vuejs/rfcs/discussions/369
* @deprecated now part of `reactivityTransform`
* @default false * @default false
*/ */
refTransform?: boolean refTransform?: boolean
/** /**
* (Experimental) Enable syntax transform for destructuring from defineProps() * (Experimental) Enable syntax transform for destructuring from defineProps()
* https://github.com/vuejs/rfcs/discussions/394 * https://github.com/vuejs/rfcs/discussions/394
* @deprecated now part of `reactivityTransform`
* @default false * @default false
*/ */
propsDestructureTransform?: boolean propsDestructureTransform?: boolean
@ -132,8 +139,13 @@ export function compileScript(
): SFCScriptBlock { ): SFCScriptBlock {
let { script, scriptSetup, source, filename } = sfc let { script, scriptSetup, source, filename } = sfc
// feature flags // feature flags
const enableRefTransform = !!options.refSugar || !!options.refTransform // TODO remove support for deprecated options when out of experimental
const enablePropsTransform = !!options.propsDestructureTransform const enableRefTransform =
!!options.reactivityTransform ||
!!options.refSugar ||
!!options.refTransform
const enablePropsTransform =
!!options.reactivityTransform || !!options.propsDestructureTransform
const isProd = !!options.isProd const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false const genSourceMap = options.sourceMap !== false
let refBindings: string[] | undefined let refBindings: string[] | undefined
@ -1097,8 +1109,7 @@ export function compileScript(
s, s,
startOffset, startOffset,
refBindings, refBindings,
propsDestructuredBindings, propsDestructuredBindings
!enableRefTransform
) )
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) { for (const h of importedHelpers) {

View File

@ -55,13 +55,12 @@ exports[`accessing ref binding 1`] = `
`; `;
exports[`array destructure 1`] = ` exports[`array destructure 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue' "import { ref as _ref, toRef as _toRef } from 'vue'
let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo()) let n = _ref(1), __$temp_1 = (useFoo()),
const a = _shallowRef(__a); a = _toRef(__$temp_1, 0),
const b = _shallowRef(__b); b = _toRef(__$temp_1, 1, 1)
const c = _shallowRef(__c); console.log(n.value, a.value, b.value)
console.log(n.value, a.value, b.value, c.value)
" "
`; `;
@ -114,13 +113,13 @@ exports[`mutating ref binding 1`] = `
`; `;
exports[`nested destructure 1`] = ` exports[`nested destructure 1`] = `
"import { shallowRef as _shallowRef } from 'vue' "import { toRef as _toRef } from 'vue'
let [{ a: { b: __b }}] = (useFoo()) let __$temp_1 = (useFoo()),
const b = _shallowRef(__b); b = _toRef(__$temp_1[0].a, 'b')
let { c: [__d, __e] } = (useBar()) let __$temp_2 = (useBar()),
const d = _shallowRef(__d); d = _toRef(__$temp_2.c, 0),
const e = _shallowRef(__e); e = _toRef(__$temp_2.c, 1)
console.log(b.value, d.value, e.value) console.log(b.value, d.value, e.value)
" "
`; `;
@ -163,20 +162,29 @@ exports[`nested scopes 1`] = `
`; `;
exports[`object destructure 1`] = ` exports[`object destructure 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue' "import { ref as _ref, toRef as _toRef } from 'vue'
let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo()) let n = _ref(1), __$temp_1 = (useFoo()),
const a = _shallowRef(__a); a = _toRef(__$temp_1, 'a'),
const c = _shallowRef(__c); c = _toRef(__$temp_1, 'b'),
const d = _shallowRef(__d); d = _toRef(__$temp_1, 'd', 1),
const f = _shallowRef(__f); f = _toRef(__$temp_1, 'e', 2),
const g = _shallowRef(__g); h = _toRef(__$temp_1, g)
let { foo: __foo } = (useSomthing(() => 1)); let __$temp_2 = (useSomthing(() => 1)),
const foo = _shallowRef(__foo); foo = _toRef(__$temp_2, 'foo');
console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value) console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)
" "
`; `;
exports[`object destructure w/ mid-path default values 1`] = `
"import { toRef as _toRef } from 'vue'
const __$temp_1 = (useFoo()),
b = _toRef((__$temp_1.a || { b: 123 }), 'b')
console.log(b.value)
"
`;
exports[`should not rewrite scope variable 1`] = ` exports[`should not rewrite scope variable 1`] = `
"import { ref as _ref } from 'vue' "import { ref as _ref } from 'vue'

View File

@ -201,40 +201,43 @@ test('should not rewrite scope variable', () => {
test('object destructure', () => { test('object destructure', () => {
const { code, rootRefs } = transform(` const { code, rootRefs } = transform(`
let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo()) let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
let { foo } = $(useSomthing(() => 1)); let { foo } = $(useSomthing(() => 1));
console.log(n, a, c, d, f, g, foo) console.log(n, a, c, d, f, h, foo)
`) `)
expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)
expect(code).toMatch(`c = _toRef(__$temp_1, 'b')`)
expect(code).toMatch(`d = _toRef(__$temp_1, 'd', 1)`)
expect(code).toMatch(`f = _toRef(__$temp_1, 'e', 2)`)
expect(code).toMatch(`h = _toRef(__$temp_1, g)`)
expect(code).toMatch(`foo = _toRef(__$temp_2, 'foo')`)
expect(code).toMatch( expect(code).toMatch(
`let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())` `console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)`
) )
expect(code).toMatch(`let { foo: __foo } = (useSomthing(() => 1))`) expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'h', 'foo'])
expect(code).toMatch(`\nconst a = _shallowRef(__a);`) assertCode(code)
expect(code).not.toMatch(`\nconst b = _shallowRef(__b);`) })
expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
expect(code).toMatch(`\nconst d = _shallowRef(__d);`) test('object destructure w/ mid-path default values', () => {
expect(code).not.toMatch(`\nconst e = _shallowRef(__e);`) const { code, rootRefs } = transform(`
expect(code).toMatch(`\nconst f = _shallowRef(__f);`) const { a: { b } = { b: 123 }} = $(useFoo())
expect(code).toMatch(`\nconst g = _shallowRef(__g);`) console.log(b)
expect(code).toMatch(`\nconst foo = _shallowRef(__foo);`) `)
expect(code).toMatch( expect(code).toMatch(`b = _toRef((__$temp_1.a || { b: 123 }), 'b')`)
`console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)` expect(code).toMatch(`console.log(b.value)`)
) expect(rootRefs).toStrictEqual(['b'])
expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
assertCode(code) assertCode(code)
}) })
test('array destructure', () => { test('array destructure', () => {
const { code, rootRefs } = transform(` const { code, rootRefs } = transform(`
let n = $ref(1), [a, b = 1, ...c] = $(useFoo()) let n = $ref(1), [a, b = 1] = $(useFoo())
console.log(n, a, b, c) console.log(n, a, b)
`) `)
expect(code).toMatch(`let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`) expect(code).toMatch(`a = _toRef(__$temp_1, 0)`)
expect(code).toMatch(`\nconst a = _shallowRef(__a);`) expect(code).toMatch(`b = _toRef(__$temp_1, 1, 1)`)
expect(code).toMatch(`\nconst b = _shallowRef(__b);`) expect(code).toMatch(`console.log(n.value, a.value, b.value)`)
expect(code).toMatch(`\nconst c = _shallowRef(__c);`) expect(rootRefs).toStrictEqual(['n', 'a', 'b'])
expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
assertCode(code) assertCode(code)
}) })
@ -244,13 +247,9 @@ test('nested destructure', () => {
let { c: [d, e] } = $(useBar()) let { c: [d, e] } = $(useBar())
console.log(b, d, e) console.log(b, d, e)
`) `)
expect(code).toMatch(`let [{ a: { b: __b }}] = (useFoo())`) expect(code).toMatch(`b = _toRef(__$temp_1[0].a, 'b')`)
expect(code).toMatch(`let { c: [__d, __e] } = (useBar())`) expect(code).toMatch(`d = _toRef(__$temp_2.c, 0)`)
expect(code).not.toMatch(`\nconst a = _shallowRef(__a);`) expect(code).toMatch(`e = _toRef(__$temp_2.c, 1)`)
expect(code).not.toMatch(`\nconst c = _shallowRef(__c);`)
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
expect(rootRefs).toStrictEqual(['b', 'd', 'e']) expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
assertCode(code) assertCode(code)
}) })
@ -396,4 +395,13 @@ describe('errors', () => {
`) `)
expect(code).not.toMatch('.value') expect(code).not.toMatch('.value')
}) })
test('rest element in $() destructure', () => {
expect(() => transform(`let { a, ...b } = $(foo())`)).toThrow(
`does not support rest element`
)
expect(() => transform(`let [a, ...b] = $(foo())`)).toThrow(
`does not support rest element`
)
})
}) })

View File

@ -4,10 +4,10 @@ import {
BlockStatement, BlockStatement,
CallExpression, CallExpression,
ObjectPattern, ObjectPattern,
VariableDeclaration,
ArrayPattern, ArrayPattern,
Program, Program,
VariableDeclarator VariableDeclarator,
Expression
} from '@babel/types' } from '@babel/types'
import MagicString, { SourceMap } from 'magic-string' import MagicString, { SourceMap } from 'magic-string'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
@ -20,7 +20,7 @@ import {
walkFunctionParams walkFunctionParams
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parse, ParserPlugin } from '@babel/parser' import { parse, ParserPlugin } from '@babel/parser'
import { hasOwn } from '@vue/shared' import { hasOwn, isArray, isString } from '@vue/shared'
const TO_VAR_SYMBOL = '$' const TO_VAR_SYMBOL = '$'
const TO_REF_SYMBOL = '$$' const TO_REF_SYMBOL = '$$'
@ -71,7 +71,7 @@ export function transform(
plugins plugins
}) })
const s = new MagicString(src) const s = new MagicString(src)
const res = transformAST(ast.program, s) const res = transformAST(ast.program, s, 0)
// inject helper imports // inject helper imports
if (res.importedHelpers.length) { if (res.importedHelpers.length) {
@ -106,16 +106,13 @@ export function transformAST(
local: string // local identifier, may be different local: string // local identifier, may be different
default?: any default?: any
} }
>, >
rewritePropsOnly = false
): { ): {
rootRefs: string[] rootRefs: string[]
importedHelpers: string[] importedHelpers: string[]
} { } {
// TODO remove when out of experimental // TODO remove when out of experimental
if (!rewritePropsOnly) { warnExperimental()
warnExperimental()
}
const importedHelpers = new Set<string>() const importedHelpers = new Set<string>()
const rootScope: Scope = {} const rootScope: Scope = {}
@ -139,7 +136,6 @@ export function transformAST(
} }
function error(msg: string, node: Node) { function error(msg: string, node: Node) {
if (rewritePropsOnly) return
const e = new Error(msg) const e = new Error(msg)
;(e as any).node = node ;(e as any).node = node
throw e throw e
@ -164,6 +160,15 @@ export function transformAST(
const registerRefBinding = (id: Identifier) => registerBinding(id, true) const registerRefBinding = (id: Identifier) => registerBinding(id, true)
let tempVarCount = 0
function genTempVar() {
return `__$temp_${++tempVarCount}`
}
function snip(node: Node) {
return s.original.slice(node.start! + offset, node.end! + offset)
}
function walkScope(node: Program | BlockStatement, isRoot = false) { function walkScope(node: Program | BlockStatement, isRoot = false) {
for (const stmt of node.body) { for (const stmt of node.body) {
if (stmt.type === 'VariableDeclaration') { if (stmt.type === 'VariableDeclaration') {
@ -180,9 +185,8 @@ export function transformAST(
) { ) {
processRefDeclaration( processRefDeclaration(
toVarCall, toVarCall,
decl.init as CallExpression,
decl.id, decl.id,
stmt decl.init as CallExpression
) )
} else { } else {
const isProps = const isProps =
@ -212,9 +216,8 @@ export function transformAST(
function processRefDeclaration( function processRefDeclaration(
method: string, method: string,
call: CallExpression,
id: VariableDeclarator['id'], id: VariableDeclarator['id'],
statement: VariableDeclaration call: CallExpression
) { ) {
excludedIds.add(call.callee as Identifier) excludedIds.add(call.callee as Identifier)
if (method === TO_VAR_SYMBOL) { if (method === TO_VAR_SYMBOL) {
@ -225,9 +228,9 @@ export function transformAST(
// single variable // single variable
registerRefBinding(id) registerRefBinding(id)
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
processRefObjectPattern(id, statement) processRefObjectPattern(id, call)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
processRefArrayPattern(id, statement) processRefArrayPattern(id, call)
} }
} else { } else {
// shorthands // shorthands
@ -247,15 +250,24 @@ export function transformAST(
function processRefObjectPattern( function processRefObjectPattern(
pattern: ObjectPattern, pattern: ObjectPattern,
statement: VariableDeclaration call: CallExpression,
tempVar?: string,
path: PathSegment[] = []
) { ) {
if (!tempVar) {
tempVar = genTempVar()
// const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
for (const p of pattern.properties) { for (const p of pattern.properties) {
let nameId: Identifier | undefined let nameId: Identifier | undefined
let key: Expression | string | undefined
let defaultValue: Expression | undefined
if (p.type === 'ObjectProperty') { if (p.type === 'ObjectProperty') {
if (p.key.start! === p.value.start!) { if (p.key.start! === p.value.start!) {
// shorthand { foo } --> { foo: __foo } // shorthand { foo }
nameId = p.key as Identifier nameId = p.key as Identifier
s.appendLeft(nameId.end! + offset, `: __${nameId.name}`)
if (p.value.type === 'Identifier') { if (p.value.type === 'Identifier') {
// avoid shorthand value identifier from being processed // avoid shorthand value identifier from being processed
excludedIds.add(p.value) excludedIds.add(p.value)
@ -265,33 +277,56 @@ export function transformAST(
) { ) {
// { foo = 1 } // { foo = 1 }
excludedIds.add(p.value.left) excludedIds.add(p.value.left)
defaultValue = p.value.right
} }
} else { } else {
key = p.computed ? p.key : (p.key as Identifier).name
if (p.value.type === 'Identifier') { if (p.value.type === 'Identifier') {
// { foo: bar } --> { foo: __bar } // { foo: bar }
nameId = p.value nameId = p.value
s.prependRight(nameId.start! + offset, `__`)
} else if (p.value.type === 'ObjectPattern') { } else if (p.value.type === 'ObjectPattern') {
processRefObjectPattern(p.value, statement) processRefObjectPattern(p.value, call, tempVar, [...path, key])
} else if (p.value.type === 'ArrayPattern') { } else if (p.value.type === 'ArrayPattern') {
processRefArrayPattern(p.value, statement) processRefArrayPattern(p.value, call, tempVar, [...path, key])
} else if (p.value.type === 'AssignmentPattern') { } else if (p.value.type === 'AssignmentPattern') {
// { foo: bar = 1 } --> { foo: __bar = 1 } if (p.value.left.type === 'Identifier') {
nameId = p.value.left as Identifier // { foo: bar = 1 }
s.prependRight(nameId.start! + offset, `__`) nameId = p.value.left
defaultValue = p.value.right
} else if (p.value.left.type === 'ObjectPattern') {
processRefObjectPattern(p.value.left, call, tempVar, [
...path,
[key, p.value.right]
])
} else if (p.value.left.type === 'ArrayPattern') {
processRefArrayPattern(p.value.left, call, tempVar, [
...path,
[key, p.value.right]
])
} else {
// MemberExpression case is not possible here, ignore
}
} }
} }
} else { } else {
// rest element { ...foo } --> { ...__foo } // rest element { ...foo }
nameId = p.argument as Identifier error(`reactivity destructure does not support rest elements.`, p)
s.prependRight(nameId.start! + offset, `__`)
} }
if (nameId) { if (nameId) {
registerRefBinding(nameId) registerRefBinding(nameId)
// append binding declarations after the parent statement // inject toRef() after original replaced pattern
const source = pathToString(tempVar, path)
const keyStr = isString(key)
? `'${key}'`
: key
? snip(key)
: `'${nameId.name}'`
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft( s.appendLeft(
statement.end! + offset, call.end! + offset,
`\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});` `,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${keyStr}${defaultStr})`
) )
} }
} }
@ -299,38 +334,80 @@ export function transformAST(
function processRefArrayPattern( function processRefArrayPattern(
pattern: ArrayPattern, pattern: ArrayPattern,
statement: VariableDeclaration call: CallExpression,
tempVar?: string,
path: PathSegment[] = []
) { ) {
for (const e of pattern.elements) { if (!tempVar) {
// const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
tempVar = genTempVar()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
for (let i = 0; i < pattern.elements.length; i++) {
const e = pattern.elements[i]
if (!e) continue if (!e) continue
let nameId: Identifier | undefined let nameId: Identifier | undefined
let defaultValue: Expression | undefined
if (e.type === 'Identifier') { if (e.type === 'Identifier') {
// [a] --> [__a] // [a] --> [__a]
nameId = e nameId = e
} else if (e.type === 'AssignmentPattern') { } else if (e.type === 'AssignmentPattern') {
// [a = 1] --> [__a = 1] // [a = 1]
nameId = e.left as Identifier nameId = e.left as Identifier
defaultValue = e.right
} else if (e.type === 'RestElement') { } else if (e.type === 'RestElement') {
// [...a] --> [...__a] // [...a]
nameId = e.argument as Identifier error(`reactivity destructure does not support rest elements.`, e)
} else if (e.type === 'ObjectPattern') { } else if (e.type === 'ObjectPattern') {
processRefObjectPattern(e, statement) processRefObjectPattern(e, call, tempVar, [...path, i])
} else if (e.type === 'ArrayPattern') { } else if (e.type === 'ArrayPattern') {
processRefArrayPattern(e, statement) processRefArrayPattern(e, call, tempVar, [...path, i])
} }
if (nameId) { if (nameId) {
registerRefBinding(nameId) registerRefBinding(nameId)
// prefix original // inject toRef() after original replaced pattern
s.prependRight(nameId.start! + offset, `__`) const source = pathToString(tempVar, path)
// append binding declarations after the parent statement const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft( s.appendLeft(
statement.end! + offset, call.end! + offset,
`\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});` `,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${i}${defaultStr})`
) )
} }
} }
} }
type PathSegmentAtom = Expression | string | number
type PathSegment =
| PathSegmentAtom
| [PathSegmentAtom, Expression /* default value */]
function pathToString(source: string, path: PathSegment[]): string {
if (path.length) {
for (const seg of path) {
if (isArray(seg)) {
source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
} else {
source += segToString(seg)
}
}
}
return source
}
function segToString(seg: PathSegmentAtom): string {
if (typeof seg === 'number') {
return `[${seg}]`
} else if (typeof seg === 'string') {
return `.${seg}`
} else {
return snip(seg)
}
}
function rewriteId( function rewriteId(
scope: Scope, scope: Scope,
id: Identifier, id: Identifier,
@ -341,10 +418,6 @@ export function transformAST(
const bindingType = scope[id.name] const bindingType = scope[id.name]
if (bindingType) { if (bindingType) {
const isProp = bindingType === 'prop' const isProp = bindingType === 'prop'
if (rewritePropsOnly && !isProp) {
return true
}
// ref
if (isStaticProperty(parent) && parent.shorthand) { if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand // let binding used in a property shorthand
// { foo } -> { foo: foo.value } // { foo } -> { foo: foo.value }
@ -498,7 +571,7 @@ function warnExperimental() {
return return
} }
warnOnce( warnOnce(
`@vue/ref-transform is an experimental feature.\n` + `Reactivity transform is an experimental feature.\n` +
`Experimental features may change behavior between patch versions.\n` + `Experimental features may change behavior between patch versions.\n` +
`It is recommended to pin your vue dependencies to exact versions to avoid breakage.\n` + `It is recommended to pin your vue dependencies to exact versions to avoid breakage.\n` +
`You can follow the proposal's status at ${RFC_LINK}.` `You can follow the proposal's status at ${RFC_LINK}.`