* https://babeljs.io/docs/en/babel-parser#plugins
*/
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`
* https://github.com/vuejs/rfcs/discussions/369
+ * @deprecated now part of `reactivityTransform`
* @default false
*/
refTransform?: boolean
/**
* (Experimental) Enable syntax transform for destructuring from defineProps()
* https://github.com/vuejs/rfcs/discussions/394
+ * @deprecated now part of `reactivityTransform`
* @default false
*/
propsDestructureTransform?: boolean
): SFCScriptBlock {
let { script, scriptSetup, source, filename } = sfc
// feature flags
- const enableRefTransform = !!options.refSugar || !!options.refTransform
- const enablePropsTransform = !!options.propsDestructureTransform
+ // TODO remove support for deprecated options when out of experimental
+ const enableRefTransform =
+ !!options.reactivityTransform ||
+ !!options.refSugar ||
+ !!options.refTransform
+ const enablePropsTransform =
+ !!options.reactivityTransform || !!options.propsDestructureTransform
const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false
let refBindings: string[] | undefined
s,
startOffset,
refBindings,
- propsDestructuredBindings,
- !enableRefTransform
+ propsDestructuredBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
`;
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())
-const a = _shallowRef(__a);
-const b = _shallowRef(__b);
-const c = _shallowRef(__c);
- console.log(n.value, a.value, b.value, c.value)
+ let n = _ref(1), __$temp_1 = (useFoo()),
+ a = _toRef(__$temp_1, 0),
+ b = _toRef(__$temp_1, 1, 1)
+ console.log(n.value, a.value, b.value)
"
`;
`;
exports[`nested destructure 1`] = `
-"import { shallowRef as _shallowRef } from 'vue'
+"import { toRef as _toRef } from 'vue'
- let [{ a: { b: __b }}] = (useFoo())
-const b = _shallowRef(__b);
- let { c: [__d, __e] } = (useBar())
-const d = _shallowRef(__d);
-const e = _shallowRef(__e);
+ let __$temp_1 = (useFoo()),
+ b = _toRef(__$temp_1[0].a, 'b')
+ let __$temp_2 = (useBar()),
+ d = _toRef(__$temp_2.c, 0),
+ e = _toRef(__$temp_2.c, 1)
console.log(b.value, d.value, e.value)
"
`;
`;
exports[`object destructure 1`] = `
-"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
-
- let n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = (useFoo())
-const a = _shallowRef(__a);
-const c = _shallowRef(__c);
-const d = _shallowRef(__d);
-const f = _shallowRef(__f);
-const g = _shallowRef(__g);
- let { foo: __foo } = (useSomthing(() => 1));
-const foo = _shallowRef(__foo);
- console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)
+"import { ref as _ref, toRef as _toRef } from 'vue'
+
+ let n = _ref(1), __$temp_1 = (useFoo()),
+ a = _toRef(__$temp_1, 'a'),
+ c = _toRef(__$temp_1, 'b'),
+ d = _toRef(__$temp_1, 'd', 1),
+ f = _toRef(__$temp_1, 'e', 2),
+ h = _toRef(__$temp_1, g)
+ let __$temp_2 = (useSomthing(() => 1)),
+ foo = _toRef(__$temp_2, 'foo');
+ 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`] = `
"import { ref as _ref } from 'vue'
test('object destructure', () => {
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));
- 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(
- `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(code).toMatch(`\nconst a = _shallowRef(__a);`)
- expect(code).not.toMatch(`\nconst b = _shallowRef(__b);`)
- expect(code).toMatch(`\nconst c = _shallowRef(__c);`)
- expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
- expect(code).not.toMatch(`\nconst e = _shallowRef(__e);`)
- expect(code).toMatch(`\nconst f = _shallowRef(__f);`)
- expect(code).toMatch(`\nconst g = _shallowRef(__g);`)
- expect(code).toMatch(`\nconst foo = _shallowRef(__foo);`)
- expect(code).toMatch(
- `console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
- )
- expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
+ expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'h', 'foo'])
+ assertCode(code)
+})
+
+test('object destructure w/ mid-path default values', () => {
+ const { code, rootRefs } = transform(`
+ const { a: { b } = { b: 123 }} = $(useFoo())
+ console.log(b)
+ `)
+ expect(code).toMatch(`b = _toRef((__$temp_1.a || { b: 123 }), 'b')`)
+ expect(code).toMatch(`console.log(b.value)`)
+ expect(rootRefs).toStrictEqual(['b'])
assertCode(code)
})
test('array destructure', () => {
const { code, rootRefs } = transform(`
- let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
- console.log(n, a, b, c)
+ let n = $ref(1), [a, b = 1] = $(useFoo())
+ console.log(n, a, b)
`)
- expect(code).toMatch(`let n = _ref(1), [__a, __b = 1, ...__c] = (useFoo())`)
- expect(code).toMatch(`\nconst a = _shallowRef(__a);`)
- 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(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
+ expect(code).toMatch(`a = _toRef(__$temp_1, 0)`)
+ expect(code).toMatch(`b = _toRef(__$temp_1, 1, 1)`)
+ expect(code).toMatch(`console.log(n.value, a.value, b.value)`)
+ expect(rootRefs).toStrictEqual(['n', 'a', 'b'])
assertCode(code)
})
let { c: [d, e] } = $(useBar())
console.log(b, d, e)
`)
- expect(code).toMatch(`let [{ a: { b: __b }}] = (useFoo())`)
- expect(code).toMatch(`let { c: [__d, __e] } = (useBar())`)
- expect(code).not.toMatch(`\nconst a = _shallowRef(__a);`)
- 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(code).toMatch(`b = _toRef(__$temp_1[0].a, 'b')`)
+ expect(code).toMatch(`d = _toRef(__$temp_2.c, 0)`)
+ expect(code).toMatch(`e = _toRef(__$temp_2.c, 1)`)
expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
assertCode(code)
})
`)
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`
+ )
+ })
})
BlockStatement,
CallExpression,
ObjectPattern,
- VariableDeclaration,
ArrayPattern,
Program,
- VariableDeclarator
+ VariableDeclarator,
+ Expression
} from '@babel/types'
import MagicString, { SourceMap } from 'magic-string'
import { walk } from 'estree-walker'
walkFunctionParams
} from '@vue/compiler-core'
import { parse, ParserPlugin } from '@babel/parser'
-import { hasOwn } from '@vue/shared'
+import { hasOwn, isArray, isString } from '@vue/shared'
const TO_VAR_SYMBOL = '$'
const TO_REF_SYMBOL = '$$'
plugins
})
const s = new MagicString(src)
- const res = transformAST(ast.program, s)
+ const res = transformAST(ast.program, s, 0)
// inject helper imports
if (res.importedHelpers.length) {
local: string // local identifier, may be different
default?: any
}
- >,
- rewritePropsOnly = false
+ >
): {
rootRefs: string[]
importedHelpers: string[]
} {
// TODO remove when out of experimental
- if (!rewritePropsOnly) {
- warnExperimental()
- }
+ warnExperimental()
const importedHelpers = new Set<string>()
const rootScope: Scope = {}
}
function error(msg: string, node: Node) {
- if (rewritePropsOnly) return
const e = new Error(msg)
;(e as any).node = node
throw e
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) {
for (const stmt of node.body) {
if (stmt.type === 'VariableDeclaration') {
) {
processRefDeclaration(
toVarCall,
- decl.init as CallExpression,
decl.id,
- stmt
+ decl.init as CallExpression
)
} else {
const isProps =
function processRefDeclaration(
method: string,
- call: CallExpression,
id: VariableDeclarator['id'],
- statement: VariableDeclaration
+ call: CallExpression
) {
excludedIds.add(call.callee as Identifier)
if (method === TO_VAR_SYMBOL) {
// single variable
registerRefBinding(id)
} else if (id.type === 'ObjectPattern') {
- processRefObjectPattern(id, statement)
+ processRefObjectPattern(id, call)
} else if (id.type === 'ArrayPattern') {
- processRefArrayPattern(id, statement)
+ processRefArrayPattern(id, call)
}
} else {
// shorthands
function processRefObjectPattern(
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) {
let nameId: Identifier | undefined
+ let key: Expression | string | undefined
+ let defaultValue: Expression | undefined
if (p.type === 'ObjectProperty') {
if (p.key.start! === p.value.start!) {
- // shorthand { foo } --> { foo: __foo }
+ // shorthand { foo }
nameId = p.key as Identifier
- s.appendLeft(nameId.end! + offset, `: __${nameId.name}`)
if (p.value.type === 'Identifier') {
// avoid shorthand value identifier from being processed
excludedIds.add(p.value)
) {
// { foo = 1 }
excludedIds.add(p.value.left)
+ defaultValue = p.value.right
}
} else {
+ key = p.computed ? p.key : (p.key as Identifier).name
if (p.value.type === 'Identifier') {
- // { foo: bar } --> { foo: __bar }
+ // { foo: bar }
nameId = p.value
- s.prependRight(nameId.start! + offset, `__`)
} else if (p.value.type === 'ObjectPattern') {
- processRefObjectPattern(p.value, statement)
+ processRefObjectPattern(p.value, call, tempVar, [...path, key])
} else if (p.value.type === 'ArrayPattern') {
- processRefArrayPattern(p.value, statement)
+ processRefArrayPattern(p.value, call, tempVar, [...path, key])
} else if (p.value.type === 'AssignmentPattern') {
- // { foo: bar = 1 } --> { foo: __bar = 1 }
- nameId = p.value.left as Identifier
- s.prependRight(nameId.start! + offset, `__`)
+ if (p.value.left.type === 'Identifier') {
+ // { foo: bar = 1 }
+ 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 {
- // rest element { ...foo } --> { ...__foo }
- nameId = p.argument as Identifier
- s.prependRight(nameId.start! + offset, `__`)
+ // rest element { ...foo }
+ error(`reactivity destructure does not support rest elements.`, p)
}
if (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(
- statement.end! + offset,
- `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
+ call.end! + offset,
+ `,\n ${nameId.name} = ${helper(
+ 'toRef'
+ )}(${source}, ${keyStr}${defaultStr})`
)
}
}
function processRefArrayPattern(
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
let nameId: Identifier | undefined
+ let defaultValue: Expression | undefined
if (e.type === 'Identifier') {
// [a] --> [__a]
nameId = e
} else if (e.type === 'AssignmentPattern') {
- // [a = 1] --> [__a = 1]
+ // [a = 1]
nameId = e.left as Identifier
+ defaultValue = e.right
} else if (e.type === 'RestElement') {
- // [...a] --> [...__a]
- nameId = e.argument as Identifier
+ // [...a]
+ error(`reactivity destructure does not support rest elements.`, e)
} else if (e.type === 'ObjectPattern') {
- processRefObjectPattern(e, statement)
+ processRefObjectPattern(e, call, tempVar, [...path, i])
} else if (e.type === 'ArrayPattern') {
- processRefArrayPattern(e, statement)
+ processRefArrayPattern(e, call, tempVar, [...path, i])
}
if (nameId) {
registerRefBinding(nameId)
- // prefix original
- s.prependRight(nameId.start! + offset, `__`)
- // append binding declarations after the parent statement
+ // inject toRef() after original replaced pattern
+ const source = pathToString(tempVar, path)
+ const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft(
- statement.end! + offset,
- `\nconst ${nameId.name} = ${helper('shallowRef')}(__${nameId.name});`
+ call.end! + offset,
+ `,\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(
scope: Scope,
id: Identifier,
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 }
return
}
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` +
`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}.`