diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index bec502741e0..1823d28cba6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -81,15 +81,22 @@ export interface SFCScriptCompileOptions { * 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 @@ -132,8 +139,13 @@ export function compileScript( ): 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 @@ -1097,8 +1109,7 @@ export function compileScript( s, startOffset, refBindings, - propsDestructuredBindings, - !enableRefTransform + propsDestructuredBindings ) refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs for (const h of importedHelpers) { diff --git a/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap b/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap index 4234d830826..5253bcce198 100644 --- a/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap +++ b/packages/ref-transform/__tests__/__snapshots__/refTransform.spec.ts.snap @@ -55,13 +55,12 @@ exports[`accessing ref binding 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()) -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) " `; @@ -114,13 +113,13 @@ exports[`mutating ref binding 1`] = ` `; 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) " `; @@ -163,20 +162,29 @@ exports[`nested scopes 1`] = ` `; 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' diff --git a/packages/ref-transform/__tests__/refTransform.spec.ts b/packages/ref-transform/__tests__/refTransform.spec.ts index 5e8775b6c52..712a234027a 100644 --- a/packages/ref-transform/__tests__/refTransform.spec.ts +++ b/packages/ref-transform/__tests__/refTransform.spec.ts @@ -201,40 +201,43 @@ test('should not rewrite scope variable', () => { 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) }) @@ -244,13 +247,9 @@ test('nested destructure', () => { 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) }) @@ -396,4 +395,13 @@ describe('errors', () => { `) 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` + ) + }) }) diff --git a/packages/ref-transform/src/refTransform.ts b/packages/ref-transform/src/refTransform.ts index 9f7a4fd72fa..a3d50d918b0 100644 --- a/packages/ref-transform/src/refTransform.ts +++ b/packages/ref-transform/src/refTransform.ts @@ -4,10 +4,10 @@ import { BlockStatement, CallExpression, ObjectPattern, - VariableDeclaration, ArrayPattern, Program, - VariableDeclarator + VariableDeclarator, + Expression } from '@babel/types' import MagicString, { SourceMap } from 'magic-string' import { walk } from 'estree-walker' @@ -20,7 +20,7 @@ import { 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 = '$$' @@ -71,7 +71,7 @@ export function transform( 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) { @@ -106,16 +106,13 @@ export function transformAST( 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() const rootScope: Scope = {} @@ -139,7 +136,6 @@ export function transformAST( } function error(msg: string, node: Node) { - if (rewritePropsOnly) return const e = new Error(msg) ;(e as any).node = node throw e @@ -164,6 +160,15 @@ export function transformAST( 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') { @@ -180,9 +185,8 @@ export function transformAST( ) { processRefDeclaration( toVarCall, - decl.init as CallExpression, decl.id, - stmt + decl.init as CallExpression ) } else { const isProps = @@ -212,9 +216,8 @@ export function transformAST( function processRefDeclaration( method: string, - call: CallExpression, id: VariableDeclarator['id'], - statement: VariableDeclaration + call: CallExpression ) { excludedIds.add(call.callee as Identifier) if (method === TO_VAR_SYMBOL) { @@ -225,9 +228,9 @@ export function transformAST( // 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 @@ -247,15 +250,24 @@ export function transformAST( 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) @@ -265,33 +277,56 @@ export function transformAST( ) { // { 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})` ) } } @@ -299,38 +334,80 @@ export function transformAST( 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, @@ -341,10 +418,6 @@ export function transformAST( 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 } @@ -498,7 +571,7 @@ function warnExperimental() { 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}.`