Skip to content

Commit 93ba6b9

Browse files
committedDec 11, 2021
feat(reactivity-transform): use toRef() for $() destructure codegen
- now supports destructuring reactive objects - no longer supports rest elements
1 parent 2db9c90 commit 93ba6b9

File tree

4 files changed

+207
-107
lines changed

4 files changed

+207
-107
lines changed
 

‎packages/compiler-sfc/src/compileScript.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,22 @@ export interface SFCScriptCompileOptions {
8181
* https://babeljs.io/docs/en/babel-parser#plugins
8282
*/
8383
babelParserPlugins?: ParserPlugin[]
84+
/**
85+
* (Experimental) Enable syntax transform for using refs without `.value` and
86+
* using destructured props with reactivity
87+
*/
88+
reactivityTransform?: boolean
8489
/**
8590
* (Experimental) Enable syntax transform for using refs without `.value`
8691
* https://github.com/vuejs/rfcs/discussions/369
92+
* @deprecated now part of `reactivityTransform`
8793
* @default false
8894
*/
8995
refTransform?: boolean
9096
/**
9197
* (Experimental) Enable syntax transform for destructuring from defineProps()
9298
* https://github.com/vuejs/rfcs/discussions/394
99+
* @deprecated now part of `reactivityTransform`
93100
* @default false
94101
*/
95102
propsDestructureTransform?: boolean
@@ -132,8 +139,13 @@ export function compileScript(
132139
): SFCScriptBlock {
133140
let { script, scriptSetup, source, filename } = sfc
134141
// feature flags
135-
const enableRefTransform = !!options.refSugar || !!options.refTransform
136-
const enablePropsTransform = !!options.propsDestructureTransform
142+
// TODO remove support for deprecated options when out of experimental
143+
const enableRefTransform =
144+
!!options.reactivityTransform ||
145+
!!options.refSugar ||
146+
!!options.refTransform
147+
const enablePropsTransform =
148+
!!options.reactivityTransform || !!options.propsDestructureTransform
137149
const isProd = !!options.isProd
138150
const genSourceMap = options.sourceMap !== false
139151
let refBindings: string[] | undefined
@@ -1097,8 +1109,7 @@ export function compileScript(
10971109
s,
10981110
startOffset,
10991111
refBindings,
1100-
propsDestructuredBindings,
1101-
!enableRefTransform
1112+
propsDestructuredBindings
11021113
)
11031114
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
11041115
for (const h of importedHelpers) {

‎packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap

+31-23
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,12 @@ exports[`accessing ref binding 1`] = `
5555
`;
5656

5757
exports[`array destructure 1`] = `
58-
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
58+
"import { ref as _ref, toRef as _toRef } from 'vue'
5959
60-
let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())
61-
const a = _shallowRef(__a);
62-
const b = _shallowRef(__b);
63-
const c = _shallowRef(__c);
64-
console.log(n.value, a.value, b.value, c.value)
60+
let n = _ref(1), __$temp_1 = (useFoo()),
61+
a = _toRef(__$temp_1, 0),
62+
b = _toRef(__$temp_1, 1, 1)
63+
console.log(n.value, a.value, b.value)
6564
"
6665
`;
6766

@@ -114,13 +113,13 @@ exports[`mutating ref binding 1`] = `
114113
`;
115114
116115
exports[`nested destructure 1`] = `
117-
"import { shallowRef as _shallowRef } from 'vue'
116+
"import { toRef as _toRef } from 'vue'
118117
119-
let [{ a: { b: __b }}] = (useFoo())
120-
const b = _shallowRef(__b);
121-
let { c: [__d, __e] } = (useBar())
122-
const d = _shallowRef(__d);
123-
const e = _shallowRef(__e);
118+
let __$temp_1 = (useFoo()),
119+
b = _toRef(__$temp_1[0].a, 'b')
120+
let __$temp_2 = (useBar()),
121+
d = _toRef(__$temp_2.c, 0),
122+
e = _toRef(__$temp_2.c, 1)
124123
console.log(b.value, d.value, e.value)
125124
"
126125
`;
@@ -163,20 +162,29 @@ exports[`nested scopes 1`] = `
163162
`;
164163
165164
exports[`object destructure 1`] = `
166-
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
167-
168-
let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())
169-
const a = _shallowRef(__a);
170-
const c = _shallowRef(__c);
171-
const d = _shallowRef(__d);
172-
const f = _shallowRef(__f);
173-
const g = _shallowRef(__g);
174-
let { foo: __foo } = (useSomthing(() => 1));
175-
const foo = _shallowRef(__foo);
176-
console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)
165+
"import { ref as _ref, toRef as _toRef } from 'vue'
166+
167+
let n = _ref(1), __$temp_1 = (useFoo()),
168+
a = _toRef(__$temp_1, 'a'),
169+
c = _toRef(__$temp_1, 'b'),
170+
d = _toRef(__$temp_1, 'd', 1),
171+
f = _toRef(__$temp_1, 'e', 2),
172+
h = _toRef(__$temp_1, g)
173+
let __$temp_2 = (useSomthing(() => 1)),
174+
foo = _toRef(__$temp_2, 'foo');
175+
console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)
177176
"
178177
`;
179178
179+
exports[`object destructure w/ mid-path default values 1`] = `
180+
"import { toRef as _toRef } from 'vue'
181+
182+
const __$temp_1 = (useFoo()),
183+
b = _toRef((__$temp_1.a || { b: 123 }), 'b')
184+
console.log(b.value)
185+
"
186+
`;
187+
180188
exports[`should not rewrite scope variable 1`] = `
181189
"import { ref as _ref } from 'vue'
182190

‎packages/ref-transform/__tests__/refTransform.spec.ts

+39-31
Original file line numberDiff line numberDiff line change
@@ -201,40 +201,43 @@ test('should not rewrite scope variable', () => {
201201

202202
test('object destructure', () => {
203203
const { code, rootRefs } = transform(`
204-
let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo())
204+
let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
205205
let { foo } = $(useSomthing(() => 1));
206-
console.log(n, a, c, d, f, g, foo)
206+
console.log(n, a, c, d, f, h, foo)
207207
`)
208+
expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)
209+
expect(code).toMatch(`c = _toRef(__$temp_1, 'b')`)
210+
expect(code).toMatch(`d = _toRef(__$temp_1, 'd', 1)`)
211+
expect(code).toMatch(`f = _toRef(__$temp_1, 'e', 2)`)
212+
expect(code).toMatch(`h = _toRef(__$temp_1, g)`)
213+
expect(code).toMatch(`foo = _toRef(__$temp_2, 'foo')`)
208214
expect(code).toMatch(
209-
`let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())`
215+
`console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)`
210216
)
211-
expect(code).toMatch(`let { foo: __foo } = (useSomthing(() => 1))`)
212-
expect(code).toMatch(`\nconst a = _shallowRef(__a);`)
213-
expect(code).not.toMatch(`\nconst b = _shallowRef(__b);`)
214-
expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
215-
expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
216-
expect(code).not.toMatch(`\nconst e = _shallowRef(__e);`)
217-
expect(code).toMatch(`\nconst f = _shallowRef(__f);`)
218-
expect(code).toMatch(`\nconst g = _shallowRef(__g);`)
219-
expect(code).toMatch(`\nconst foo = _shallowRef(__foo);`)
220-
expect(code).toMatch(
221-
`console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
222-
)
223-
expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
217+
expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'h', 'foo'])
218+
assertCode(code)
219+
})
220+
221+
test('object destructure w/ mid-path default values', () => {
222+
const { code, rootRefs } = transform(`
223+
const { a: { b } = { b: 123 }} = $(useFoo())
224+
console.log(b)
225+
`)
226+
expect(code).toMatch(`b = _toRef((__$temp_1.a || { b: 123 }), 'b')`)
227+
expect(code).toMatch(`console.log(b.value)`)
228+
expect(rootRefs).toStrictEqual(['b'])
224229
assertCode(code)
225230
})
226231

227232
test('array destructure', () => {
228233
const { code, rootRefs } = transform(`
229-
let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
230-
console.log(n, a, b, c)
234+
let n = $ref(1), [a, b = 1] = $(useFoo())
235+
console.log(n, a, b)
231236
`)
232-
expect(code).toMatch(`let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`)
233-
expect(code).toMatch(`\nconst a = _shallowRef(__a);`)
234-
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
235-
expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
236-
expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
237-
expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
237+
expect(code).toMatch(`a = _toRef(__$temp_1, 0)`)
238+
expect(code).toMatch(`b = _toRef(__$temp_1, 1, 1)`)
239+
expect(code).toMatch(`console.log(n.value, a.value, b.value)`)
240+
expect(rootRefs).toStrictEqual(['n', 'a', 'b'])
238241
assertCode(code)
239242
})
240243

@@ -244,13 +247,9 @@ test('nested destructure', () => {
244247
let { c: [d, e] } = $(useBar())
245248
console.log(b, d, e)
246249
`)
247-
expect(code).toMatch(`let [{ a: { b: __b }}] = (useFoo())`)
248-
expect(code).toMatch(`let { c: [__d, __e] } = (useBar())`)
249-
expect(code).not.toMatch(`\nconst a = _shallowRef(__a);`)
250-
expect(code).not.toMatch(`\nconst c = _shallowRef(__c);`)
251-
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
252-
expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
253-
expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
250+
expect(code).toMatch(`b = _toRef(__$temp_1[0].a, 'b')`)
251+
expect(code).toMatch(`d = _toRef(__$temp_2.c, 0)`)
252+
expect(code).toMatch(`e = _toRef(__$temp_2.c, 1)`)
254253
expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
255254
assertCode(code)
256255
})
@@ -396,4 +395,13 @@ describe('errors', () => {
396395
`)
397396
expect(code).not.toMatch('.value')
398397
})
398+
399+
test('rest element in $() destructure', () => {
400+
expect(() => transform(`let { a, ...b } = $(foo())`)).toThrow(
401+
`does not support rest element`
402+
)
403+
expect(() => transform(`let [a, ...b] = $(foo())`)).toThrow(
404+
`does not support rest element`
405+
)
406+
})
399407
})

‎packages/ref-transform/src/refTransform.ts

+122-49
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {
44
BlockStatement,
55
CallExpression,
66
ObjectPattern,
7-
VariableDeclaration,
87
ArrayPattern,
98
Program,
10-
VariableDeclarator
9+
VariableDeclarator,
10+
Expression
1111
} from '@babel/types'
1212
import MagicString, { SourceMap } from 'magic-string'
1313
import { walk } from 'estree-walker'
@@ -20,7 +20,7 @@ import {
2020
walkFunctionParams
2121
} from '@vue/compiler-core'
2222
import { parse, ParserPlugin } from '@babel/parser'
23-
import { hasOwn } from '@vue/shared'
23+
import { hasOwn, isArray, isString } from '@vue/shared'
2424

2525
const TO_VAR_SYMBOL = '$'
2626
const TO_REF_SYMBOL = '$$'
@@ -71,7 +71,7 @@ export function transform(
7171
plugins
7272
})
7373
const s = new MagicString(src)
74-
const res = transformAST(ast.program, s)
74+
const res = transformAST(ast.program, s, 0)
7575

7676
// inject helper imports
7777
if (res.importedHelpers.length) {
@@ -106,16 +106,13 @@ export function transformAST(
106106
local: string // local identifier, may be different
107107
default?: any
108108
}
109-
>,
110-
rewritePropsOnly = false
109+
>
111110
): {
112111
rootRefs: string[]
113112
importedHelpers: string[]
114113
} {
115114
// TODO remove when out of experimental
116-
if (!rewritePropsOnly) {
117-
warnExperimental()
118-
}
115+
warnExperimental()
119116

120117
const importedHelpers = new Set<string>()
121118
const rootScope: Scope = {}
@@ -139,7 +136,6 @@ export function transformAST(
139136
}
140137

141138
function error(msg: string, node: Node) {
142-
if (rewritePropsOnly) return
143139
const e = new Error(msg)
144140
;(e as any).node = node
145141
throw e
@@ -164,6 +160,15 @@ export function transformAST(
164160

165161
const registerRefBinding = (id: Identifier) => registerBinding(id, true)
166162

163+
let tempVarCount = 0
164+
function genTempVar() {
165+
return `__$temp_${++tempVarCount}`
166+
}
167+
168+
function snip(node: Node) {
169+
return s.original.slice(node.start! + offset, node.end! + offset)
170+
}
171+
167172
function walkScope(node: Program | BlockStatement, isRoot = false) {
168173
for (const stmt of node.body) {
169174
if (stmt.type === 'VariableDeclaration') {
@@ -180,9 +185,8 @@ export function transformAST(
180185
) {
181186
processRefDeclaration(
182187
toVarCall,
183-
decl.init as CallExpression,
184188
decl.id,
185-
stmt
189+
decl.init as CallExpression
186190
)
187191
} else {
188192
const isProps =
@@ -212,9 +216,8 @@ export function transformAST(
212216

213217
function processRefDeclaration(
214218
method: string,
215-
call: CallExpression,
216219
id: VariableDeclarator['id'],
217-
statement: VariableDeclaration
220+
call: CallExpression
218221
) {
219222
excludedIds.add(call.callee as Identifier)
220223
if (method === TO_VAR_SYMBOL) {
@@ -225,9 +228,9 @@ export function transformAST(
225228
// single variable
226229
registerRefBinding(id)
227230
} else if (id.type === 'ObjectPattern') {
228-
processRefObjectPattern(id, statement)
231+
processRefObjectPattern(id, call)
229232
} else if (id.type === 'ArrayPattern') {
230-
processRefArrayPattern(id, statement)
233+
processRefArrayPattern(id, call)
231234
}
232235
} else {
233236
// shorthands
@@ -247,15 +250,24 @@ export function transformAST(
247250

248251
function processRefObjectPattern(
249252
pattern: ObjectPattern,
250-
statement: VariableDeclaration
253+
call: CallExpression,
254+
tempVar?: string,
255+
path: PathSegment[] = []
251256
) {
257+
if (!tempVar) {
258+
tempVar = genTempVar()
259+
// const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
260+
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
261+
}
262+
252263
for (const p of pattern.properties) {
253264
let nameId: Identifier | undefined
265+
let key: Expression | string | undefined
266+
let defaultValue: Expression | undefined
254267
if (p.type === 'ObjectProperty') {
255268
if (p.key.start! === p.value.start!) {
256-
// shorthand { foo } --> { foo: __foo }
269+
// shorthand { foo }
257270
nameId = p.key as Identifier
258-
s.appendLeft(nameId.end! + offset, `: __${nameId.name}`)
259271
if (p.value.type === 'Identifier') {
260272
// avoid shorthand value identifier from being processed
261273
excludedIds.add(p.value)
@@ -265,72 +277,137 @@ export function transformAST(
265277
) {
266278
// { foo = 1 }
267279
excludedIds.add(p.value.left)
280+
defaultValue = p.value.right
268281
}
269282
} else {
283+
key = p.computed ? p.key : (p.key as Identifier).name
270284
if (p.value.type === 'Identifier') {
271-
// { foo: bar } --> { foo: __bar }
285+
// { foo: bar }
272286
nameId = p.value
273-
s.prependRight(nameId.start! + offset, `__`)
274287
} else if (p.value.type === 'ObjectPattern') {
275-
processRefObjectPattern(p.value, statement)
288+
processRefObjectPattern(p.value, call, tempVar, [...path, key])
276289
} else if (p.value.type === 'ArrayPattern') {
277-
processRefArrayPattern(p.value, statement)
290+
processRefArrayPattern(p.value, call, tempVar, [...path, key])
278291
} else if (p.value.type === 'AssignmentPattern') {
279-
// { foo: bar = 1 } --> { foo: __bar = 1 }
280-
nameId = p.value.left as Identifier
281-
s.prependRight(nameId.start! + offset, `__`)
292+
if (p.value.left.type === 'Identifier') {
293+
// { foo: bar = 1 }
294+
nameId = p.value.left
295+
defaultValue = p.value.right
296+
} else if (p.value.left.type === 'ObjectPattern') {
297+
processRefObjectPattern(p.value.left, call, tempVar, [
298+
...path,
299+
[key, p.value.right]
300+
])
301+
} else if (p.value.left.type === 'ArrayPattern') {
302+
processRefArrayPattern(p.value.left, call, tempVar, [
303+
...path,
304+
[key, p.value.right]
305+
])
306+
} else {
307+
// MemberExpression case is not possible here, ignore
308+
}
282309
}
283310
}
284311
} else {
285-
// rest element { ...foo } --> { ...__foo }
286-
nameId = p.argument as Identifier
287-
s.prependRight(nameId.start! + offset, `__`)
312+
// rest element { ...foo }
313+
error(`reactivity destructure does not support rest elements.`, p)
288314
}
289315
if (nameId) {
290316
registerRefBinding(nameId)
291-
// append binding declarations after the parent statement
317+
// inject toRef() after original replaced pattern
318+
const source = pathToString(tempVar, path)
319+
const keyStr = isString(key)
320+
? `'${key}'`
321+
: key
322+
? snip(key)
323+
: `'${nameId.name}'`
324+
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
292325
s.appendLeft(
293-
statement.end! + offset,
294-
`\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
326+
call.end! + offset,
327+
`,\n ${nameId.name} = ${helper(
328+
'toRef'
329+
)}(${source}, ${keyStr}${defaultStr})`
295330
)
296331
}
297332
}
298333
}
299334

300335
function processRefArrayPattern(
301336
pattern: ArrayPattern,
302-
statement: VariableDeclaration
337+
call: CallExpression,
338+
tempVar?: string,
339+
path: PathSegment[] = []
303340
) {
304-
for (const e of pattern.elements) {
341+
if (!tempVar) {
342+
// const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
343+
tempVar = genTempVar()
344+
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
345+
}
346+
347+
for (let i = 0; i < pattern.elements.length; i++) {
348+
const e = pattern.elements[i]
305349
if (!e) continue
306350
let nameId: Identifier | undefined
351+
let defaultValue: Expression | undefined
307352
if (e.type === 'Identifier') {
308353
// [a] --> [__a]
309354
nameId = e
310355
} else if (e.type === 'AssignmentPattern') {
311-
// [a = 1] --> [__a = 1]
356+
// [a = 1]
312357
nameId = e.left as Identifier
358+
defaultValue = e.right
313359
} else if (e.type === 'RestElement') {
314-
// [...a] --> [...__a]
315-
nameId = e.argument as Identifier
360+
// [...a]
361+
error(`reactivity destructure does not support rest elements.`, e)
316362
} else if (e.type === 'ObjectPattern') {
317-
processRefObjectPattern(e, statement)
363+
processRefObjectPattern(e, call, tempVar, [...path, i])
318364
} else if (e.type === 'ArrayPattern') {
319-
processRefArrayPattern(e, statement)
365+
processRefArrayPattern(e, call, tempVar, [...path, i])
320366
}
321367
if (nameId) {
322368
registerRefBinding(nameId)
323-
// prefix original
324-
s.prependRight(nameId.start! + offset, `__`)
325-
// append binding declarations after the parent statement
369+
// inject toRef() after original replaced pattern
370+
const source = pathToString(tempVar, path)
371+
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
326372
s.appendLeft(
327-
statement.end! + offset,
328-
`\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
373+
call.end! + offset,
374+
`,\n ${nameId.name} = ${helper(
375+
'toRef'
376+
)}(${source}, ${i}${defaultStr})`
329377
)
330378
}
331379
}
332380
}
333381

382+
type PathSegmentAtom = Expression | string | number
383+
384+
type PathSegment =
385+
| PathSegmentAtom
386+
| [PathSegmentAtom, Expression /* default value */]
387+
388+
function pathToString(source: string, path: PathSegment[]): string {
389+
if (path.length) {
390+
for (const seg of path) {
391+
if (isArray(seg)) {
392+
source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
393+
} else {
394+
source += segToString(seg)
395+
}
396+
}
397+
}
398+
return source
399+
}
400+
401+
function segToString(seg: PathSegmentAtom): string {
402+
if (typeof seg === 'number') {
403+
return `[${seg}]`
404+
} else if (typeof seg === 'string') {
405+
return `.${seg}`
406+
} else {
407+
return snip(seg)
408+
}
409+
}
410+
334411
function rewriteId(
335412
scope: Scope,
336413
id: Identifier,
@@ -341,10 +418,6 @@ export function transformAST(
341418
const bindingType = scope[id.name]
342419
if (bindingType) {
343420
const isProp = bindingType === 'prop'
344-
if (rewritePropsOnly && !isProp) {
345-
return true
346-
}
347-
// ref
348421
if (isStaticProperty(parent) && parent.shorthand) {
349422
// let binding used in a property shorthand
350423
// { foo } -> { foo: foo.value }
@@ -498,7 +571,7 @@ function warnExperimental() {
498571
return
499572
}
500573
warnOnce(
501-
`@vue/ref-transform is an experimental feature.\n` +
574+
`Reactivity transform is an experimental feature.\n` +
502575
`Experimental features may change behavior between patch versions.\n` +
503576
`It is recommended to pin your vue dependencies to exact versions to avoid breakage.\n` +
504577
`You can follow the proposal's status at ${RFC_LINK}.`

0 commit comments

Comments
 (0)
Please sign in to comment.