feat!: remove reactivity transform (#9321)

BREAKING CHANGE: Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html
This commit is contained in:
三咲智子 Kevin Deng 2023-11-21 18:06:42 +08:00 committed by GitHub
parent afb21f7813
commit 79b8a0905b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 43 additions and 2425 deletions

View File

@ -52,9 +52,7 @@ module.exports = {
},
// Packages targeting Node
{
files: [
'packages/{compiler-sfc,compiler-ssr,server-renderer,reactivity-transform}/**'
],
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
rules: {
'no-restricted-globals': ['error', ...DOMGlobals],
'no-restricted-syntax': 'off'

View File

@ -33,7 +33,7 @@
"serve": "serve",
"open": "open http://localhost:3000/packages/template-explorer/local.html",
"build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity reactivity-transform shared -af cjs",
"build-all-cjs": "node scripts/build.js vue runtime compiler reactivity shared -af cjs",
"build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
"build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
"build-sfc-playground-self": "cd packages/sfc-playground && npm run build",

View File

@ -515,20 +515,20 @@ return { }
`;
exports[`SFC compile <script setup> > async/await detection > ref 1`] = `
"import { withAsyncContext as _withAsyncContext, ref as _ref } from 'vue'
"import { withAsyncContext as _withAsyncContext } from 'vue'
export default {
async setup(__props, { expose: __expose }) {
__expose();
let __temp, __restore
let a = _ref(1 + ((
let a = ref(1 + ((
([__temp,__restore] = _withAsyncContext(() => foo)),
__temp = await __temp,
__restore(),
__temp
)))
return { a }
return { get a() { return a }, set a(v) { a = v } }
}
}"
@ -818,16 +818,20 @@ return { bar }
`;
exports[`SFC compile <script setup> > imports > dedupe between user & helper 1`] = `
"import { ref as _ref } from 'vue'
import { ref } from 'vue'
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { useCssVars, ref } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = _ref(1)
_useCssVars(_ctx => ({
\\"xxxxxxxx-msg\\": (msg.value)
}))
const msg = ref()
return { foo, ref }
return { msg, useCssVars, ref }
}
}"

View File

@ -240,14 +240,22 @@ describe('SFC compile <script setup>', () => {
const { content } = compile(
`
<script setup>
import { ref } from 'vue'
let foo = $ref(1)
import { useCssVars, ref } from 'vue'
const msg = ref()
</script>
`,
{ reactivityTransform: true }
<style>
.foo {
color: v-bind(msg)
}
</style>
`
)
assertCode(content)
expect(content).toMatch(`import { ref } from 'vue'`)
expect(content).toMatch(
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`
)
expect(content).toMatch(`import { useCssVars, ref } from 'vue'`)
})
test('import dedupe between <script> and <script setup>', () => {
@ -891,9 +899,7 @@ describe('SFC compile <script setup>', () => {
describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`, {
reactivityTransform: true
})
const { content } = compile(`<script setup>${code}</script>`)
if (shouldAsync) {
expect(content).toMatch(`let __temp, __restore`)
}
@ -911,7 +917,7 @@ describe('SFC compile <script setup>', () => {
})
test('ref', () => {
assertAwaitDetection(`let a = $ref(1 + (await foo))`)
assertAwaitDetection(`let a = ref(1 + (await foo))`)
})
// #4448

View File

@ -1,113 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sfc ref transform > $ unwrapping 1`] = `
"import { ref, shallowRef } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = (ref())
let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
return { foo, a, b, get c() { return c }, set c(v) { c = v }, get d() { return d }, set d(v) { d = v }, ref, shallowRef }
}
}"
`;
exports[`sfc ref transform > $ref & $shallowRef declarations 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
export default {
setup(__props, { expose: __expose }) {
__expose();
let foo = _ref()
let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
return { foo, a, b, get c() { return c }, set c(v) { c = v }, get d() { return d }, set d(v) { d = v } }
}
}"
`;
exports[`sfc ref transform > usage /w typescript 1`] = `
"import { ref as _ref, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
let msg = _ref<string | number>('foo');
let bar = _ref <string | number>('bar');
return { msg, bar }
}
})"
`;
exports[`sfc ref transform > usage in normal <script> 1`] = `
"import { ref as _ref } from 'vue'
export default {
setup() {
let count = _ref(0)
const inc = () => count.value++
return ({ count })
}
}
"
`;
exports[`sfc ref transform > usage with normal <script> (has macro usage) + <script setup> (no macro usage) 1`] = `
"import { ref as _ref } from 'vue'
let data = _ref()
export default {
setup(__props, { expose: __expose }) {
__expose();
console.log(data.value)
return { data }
}
}"
`;
exports[`sfc ref transform > usage with normal <script> + <script setup> 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(0)
let c = _ref(0)
export default {
setup(__props, { expose: __expose }) {
__expose();
let b = _ref(0)
let c = 0
function change() {
a.value++
b.value++
c++
}
return { a, c, b, change }
}
}"
`;

View File

@ -1,193 +0,0 @@
// TODO remove in 3.4
import { BindingTypes } from '@vue/compiler-core'
import { compileSFCScript as compile, assertCode } from '../utils'
// this file only tests integration with SFC - main test case for the ref
// transform can be found in <root>/packages/reactivity-transform/__tests__
describe('sfc ref transform', () => {
function compileWithReactivityTransform(src: string) {
return compile(src, { reactivityTransform: true })
}
test('$ unwrapping', () => {
const { content, bindings } = compileWithReactivityTransform(`<script setup>
import { ref, shallowRef } from 'vue'
let foo = $(ref())
let a = $(ref(1))
let b = $(shallowRef({
count: 0
}))
let c = () => {}
let d
</script>`)
expect(content).not.toMatch(`$(ref())`)
expect(content).not.toMatch(`$(ref(1))`)
expect(content).not.toMatch(`$(shallowRef({`)
expect(content).toMatch(`let foo = (ref())`)
expect(content).toMatch(`let a = (ref(1))`)
expect(content).toMatch(`
let b = (shallowRef({
count: 0
}))
`)
// normal declarations left untouched
expect(content).toMatch(`let c = () => {}`)
expect(content).toMatch(`let d`)
expect(content).toMatch(
`return { foo, a, b, get c() { return c }, set c(v) { c = v }, ` +
`get d() { return d }, set d(v) { d = v }, ref, shallowRef }`
)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET,
ref: BindingTypes.SETUP_CONST,
shallowRef: BindingTypes.SETUP_CONST
})
})
test('$ref & $shallowRef declarations', () => {
const { content, bindings } = compileWithReactivityTransform(`<script setup>
let foo = $ref()
let a = $ref(1)
let b = $shallowRef({
count: 0
})
let c = () => {}
let d
</script>`)
expect(content).toMatch(
`import { ref as _ref, shallowRef as _shallowRef } from 'vue'`
)
expect(content).not.toMatch(`$ref()`)
expect(content).not.toMatch(`$ref(1)`)
expect(content).not.toMatch(`$shallowRef({`)
expect(content).toMatch(`let foo = _ref()`)
expect(content).toMatch(`let a = _ref(1)`)
expect(content).toMatch(`
let b = _shallowRef({
count: 0
})
`)
// normal declarations left untouched
expect(content).toMatch(`let c = () => {}`)
expect(content).toMatch(`let d`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_REF,
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET
})
})
test('usage in normal <script>', () => {
const { content } = compileWithReactivityTransform(`<script>
export default {
setup() {
let count = $ref(0)
const inc = () => count++
return $$({ count })
}
}
</script>`)
expect(content).not.toMatch(`$ref(0)`)
expect(content).toMatch(`import { ref as _ref } from 'vue'`)
expect(content).toMatch(`let count = _ref(0)`)
expect(content).toMatch(`count.value++`)
expect(content).toMatch(`return ({ count })`)
assertCode(content)
})
test('usage /w typescript', () => {
const { content } = compileWithReactivityTransform(`
<script setup lang="ts">
let msg = $ref<string | number>('foo');
let bar = $ref <string | number>('bar');
</script>
`)
expect(content).toMatch(`import { ref as _ref`)
expect(content).toMatch(`let msg = _ref<string | number>('foo')`)
expect(content).toMatch(`let bar = _ref <string | number>('bar')`)
assertCode(content)
})
test('usage with normal <script> + <script setup>', () => {
const { content, bindings } = compileWithReactivityTransform(`<script>
let a = $ref(0)
let c = $ref(0)
</script>
<script setup>
let b = $ref(0)
let c = 0
function change() {
a++
b++
c++
}
</script>`)
// should dedupe helper imports
expect(content).toMatch(`import { ref as _ref } from 'vue'`)
expect(content).toMatch(`let a = _ref(0)`)
expect(content).toMatch(`let b = _ref(0)`)
// root level ref binding declared in <script> should be inherited in <script setup>
expect(content).toMatch(`a.value++`)
expect(content).toMatch(`b.value++`)
// c shadowed
expect(content).toMatch(`c++`)
assertCode(content)
expect(bindings).toStrictEqual({
a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_REF,
change: BindingTypes.SETUP_CONST
})
})
test('usage with normal <script> (has macro usage) + <script setup> (no macro usage)', () => {
const { content } = compileWithReactivityTransform(`
<script>
let data = $ref()
</script>
<script setup>
console.log(data)
</script>
`)
expect(content).toMatch(`console.log(data.value)`)
assertCode(content)
})
describe('errors', () => {
test('defineProps/Emit() referencing ref declarations', () => {
expect(() =>
compile(
`<script setup>
let bar = $ref(1)
defineProps({
bar
})
</script>`,
{ reactivityTransform: true }
)
).toThrow(`cannot reference locally declared variables`)
expect(() =>
compile(
`<script setup>
let bar = $ref(1)
defineEmits({
bar
})
</script>`,
{ reactivityTransform: true }
)
).toThrow(`cannot reference locally declared variables`)
})
})
})

View File

@ -36,7 +36,6 @@
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/reactivity-transform": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",

View File

@ -26,7 +26,6 @@ import {
import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnOnce } from './warn'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { transformDestructuredProps } from './script/definePropsDestructure'
import { ScriptCompileContext } from './script/context'
import {
@ -122,14 +121,6 @@ export interface SFCScriptCompileOptions {
fileExists(file: string): boolean
readFile(file: string): string | undefined
}
/**
* (Experimental) Enable syntax transform for using refs without `.value` and
* using destructured props with reactivity
* @deprecated the Reactivity Transform proposal has been dropped. This
* feature will be removed from Vue core in 3.4. If you intend to continue
* using it, disable this and switch to the [Vue Macros implementation](https://vue-macros.sxzz.moe/features/reactivity-transform.html).
*/
reactivityTransform?: boolean
}
export interface ImportBinding {
@ -165,8 +156,6 @@ export function compileScript(
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
// TODO remove in 3.4
const enableReactivityTransform = !!options.reactivityTransform
let refBindings: string[] | undefined
if (!scriptSetup) {
@ -477,20 +466,6 @@ export function compileScript(
}
}
// apply reactivity transform
// TODO remove in 3.4
if (enableReactivityTransform && shouldTransform(script.content)) {
const { rootRefs, importedHelpers } = transformAST(
scriptAst,
ctx.s,
scriptStartOffset!
)
refBindings = rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// <script> after <script setup>
// we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition
@ -686,26 +661,7 @@ export function compileScript(
transformDestructuredProps(ctx, vueImportAliases)
}
// 4. Apply reactivity transform
// TODO remove in 3.4
if (
enableReactivityTransform &&
// normal <script> had ref bindings that maybe used in <script setup>
(refBindings || shouldTransform(scriptSetup.content))
) {
const { rootRefs, importedHelpers } = transformAST(
scriptSetupAst,
ctx.s,
startOffset,
refBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
ctx.helperImports.add(h)
}
}
// 5. check macro args to make sure it doesn't reference setup scope
// 4. check macro args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
@ -713,7 +669,7 @@ export function compileScript(
checkInvalidScopeReference(ctx.emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(ctx.optionsRuntimeDecl, DEFINE_OPTIONS)
// 6. remove non-script content
// 5. remove non-script content
if (script) {
if (startOffset < scriptStartOffset!) {
// <script setup> before <script>
@ -732,7 +688,7 @@ export function compileScript(
ctx.s.remove(endOffset, source.length)
}
// 7. analyze binding metadata
// 6. analyze binding metadata
// `defineProps` & `defineModel` also register props bindings
if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
@ -761,7 +717,7 @@ export function compileScript(
}
}
// 8. inject `useCssVars` calls
// 7. inject `useCssVars` calls
if (
sfc.cssVars.length &&
// no need to do this when targeting SSR
@ -780,7 +736,7 @@ export function compileScript(
)
}
// 9. finalize setup() argument signature
// 8. finalize setup() argument signature
let args = `__props`
if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment
@ -831,7 +787,7 @@ export function compileScript(
args += `, { ${destructureElements.join(', ')} }`
}
// 10. generate return statement
// 9. generate return statement
let returned
if (
!options.inlineTemplate ||
@ -947,7 +903,7 @@ export function compileScript(
ctx.s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
}
// 11. finalize default export
// 10. finalize default export
const genDefaultAs = options.genDefaultAs
? `const ${options.genDefaultAs} =`
: `export default`
@ -1021,7 +977,7 @@ export function compileScript(
}
}
// 12. finalize Vue helper imports
// 11. finalize Vue helper imports
if (ctx.helperImports.size > 0) {
ctx.s.prepend(
`import { ${[...ctx.helperImports]

View File

@ -12,13 +12,6 @@ import { SFCParseResult, parseCache as _parseCache } from './parse'
// #9521 export parseCache as a simple map to avoid exposing LRU types
export const parseCache = _parseCache as Map<string, SFCParseResult>
// TODO remove in 3.4
export {
shouldTransform as shouldTransformRef,
transform as transformRef,
transformAST as transformRefAST
} from '@vue/reactivity-transform'
// Utilities
export { parse as babelParse } from '@babel/parser'
import MagicString from 'magic-string'

View File

@ -27,7 +27,7 @@ export function processPropsDestructure(
ctx: ScriptCompileContext,
declId: ObjectPattern
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
if (!ctx.options.propsDestructure) {
return
}
@ -103,7 +103,7 @@ export function transformDestructuredProps(
ctx: ScriptCompileContext,
vueImportAliases: Record<string, string>
) {
if (!ctx.options.propsDestructure && !ctx.options.reactivityTransform) {
if (!ctx.options.propsDestructure) {
return
}

View File

@ -1,8 +1,6 @@
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
import { analyzeScriptBindings } from './analyzeScriptBindings'
import { ScriptCompileContext } from './context'
import MagicString from 'magic-string'
import { RawSourceMap } from 'source-map-js'
import { rewriteDefaultAST } from '../rewriteDefault'
import { genNormalScriptCssVarsCode } from '../style/cssVars'
@ -22,33 +20,8 @@ export function processNormalScript(
let map = script.map
const scriptAst = ctx.scriptAst!
const bindings = analyzeScriptBindings(scriptAst.body)
const { source, filename, cssVars } = ctx.descriptor
const { sourceMap, genDefaultAs, isProd } = ctx.options
// TODO remove in 3.4
if (ctx.options.reactivityTransform && shouldTransform(content)) {
const s = new MagicString(source)
const startOffset = script.loc.start.offset
const endOffset = script.loc.end.offset
const { importedHelpers } = transformAST(scriptAst, s, startOffset)
if (importedHelpers.length) {
s.prepend(
`import { ${importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}
s.remove(0, startOffset)
s.remove(endOffset, source.length)
content = s.toString()
if (sourceMap !== false) {
map = s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap
}
}
const { cssVars } = ctx.descriptor
const { genDefaultAs, isProd } = ctx.options
if (cssVars.length || genDefaultAs) {
const defaultVar = genDefaultAs || normalScriptDefaultVar

View File

@ -1,110 +0,0 @@
import { ref, computed, Ref, ComputedRef, WritableComputedRef } from 'vue'
import 'vue/macros-global'
import { RefType, RefTypes } from 'vue/macros'
import { expectType } from './utils'
// wrapping refs
// normal
let n = $(ref(1))
n = 2
// @ts-expect-error
n = 'foo'
// #4499 nullable
let msg = $(ref<string | null>(null))
msg = 'hello world'
msg = null
expectType<RefTypes.Ref | undefined>(msg![RefType])
// computed
let m = $(computed(() => n + 1))
m * 1
// @ts-expect-error
m.slice()
expectType<RefTypes.ComputedRef | undefined>(m[RefType])
// writable computed
let wc = $(
computed({
get: () => n + 1,
set: v => (n = v - 1)
})
)
wc = 2
// @ts-expect-error
wc = 'foo'
expectType<RefTypes.WritableComputedRef | undefined>(wc[RefType])
// destructure
function useFoo() {
let x = $ref(1)
let y = $computed(() => 'hi')
return $$({
x,
y,
z: 123
})
}
const fooRes = useFoo()
const { x, y, z } = $(fooRes)
expectType<number>(x)
expectType<string>(y)
expectType<number>(z)
// $ref
expectType<number>($ref(1))
expectType<number>($ref(ref(1)))
expectType<{ foo: number }>($ref({ foo: ref(1) }))
// $shallowRef
expectType<number>($shallowRef(1))
expectType<{ foo: Ref<number> }>($shallowRef({ foo: ref(1) }))
// $computed
expectType<number>($computed(() => 1))
let b = $ref(1)
expectType<number>(
$computed(() => b, {
onTrack() {}
})
)
// writable computed
expectType<number>(
$computed({
get: () => 1,
set: () => {}
})
)
// $$
const xRef = $$(x)
expectType<Ref<number>>(xRef)
const yRef = $$(y)
expectType<ComputedRef<string>>(yRef)
const c = $computed(() => 1)
const cRef = $$(c)
expectType<ComputedRef<number>>(cRef)
const c2 = $computed({
get: () => 1,
set: () => {}
})
const c2Ref = $$(c2)
expectType<WritableComputedRef<number>>(c2Ref)
// $$ on object
const obj = $$({
n,
m,
wc
})
expectType<Ref<number>>(obj.n)
expectType<ComputedRef<number>>(obj.m)
expectType<WritableComputedRef<number>>(obj.wc)

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,123 +0,0 @@
# @vue/reactivity-transform
> ⚠️ This is experimental and the proposal has been dropped.
> The feature is now marked as deprecated and will be removed from Vue core
> in 3.4.
>
> See reason for deprecation [here](https://github.com/vuejs/rfcs/discussions/369#discussioncomment-5059028).
## Basic Rules
- Ref-creating APIs have `$`-prefixed versions that create reactive variables instead. They also do not need to be explicitly imported. These include:
- `ref`
- `computed`
- `shallowRef`
- `customRef`
- `toRef`
- `$()` can be used to destructure an object into reactive variables, or turn existing refs into reactive variables
- `$$()` to "escape" the transform, which allows access to underlying refs
```js
import { watchEffect } from 'vue'
// bind ref as a variable
let count = $ref(0)
watchEffect(() => {
// no need for .value
console.log(count)
})
// assignments are reactive
count++
// get the actual ref
console.log($$(count)) // { value: 1 }
```
Macros can be optionally imported to make it more explicit:
```js
// not necessary, but also works
import { $, $ref } from 'vue/macros'
let count = $ref(0)
const { x, y } = $(useMouse())
```
### Global Types
To enable types for the macros globally, include the following in a `.d.ts` file:
```ts
/// <reference types="vue/macros-global" />
```
## API
This package is the lower-level transform that can be used standalone. Higher-level tooling (e.g. `@vitejs/plugin-vue` and `vue-loader`) will provide integration via options.
### `shouldTransform`
Can be used to do a cheap check to determine whether full transform should be performed.
```js
import { shouldTransform } from '@vue/reactivity-transform'
shouldTransform(`let a = ref(0)`) // false
shouldTransform(`let a = $ref(0)`) // true
```
### `transform`
```js
import { transform } from '@vue/reactivity-transform'
const src = `let a = $ref(0); a++`
const {
code, // import { ref as _ref } from 'vue'; let a = (ref(0)); a.value++"
map
} = transform(src, {
filename: 'foo.ts',
sourceMap: true,
// @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: [
/* ... */
]
})
```
**Options**
```ts
interface RefTransformOptions {
filename?: string
sourceMap?: boolean // default: false
parserPlugins?: ParserPlugin[]
importHelpersFrom?: string // default: "vue"
}
```
### `transformAST`
Transform with an existing Babel AST + MagicString instance. This is used internally by `@vue/compiler-sfc` to avoid double parse/transform cost.
```js
import { transformAST } from '@vue/reactivity-transform'
import { parse } from '@babel/parser'
import MagicString from 'magic-string'
const src = `let a = $ref(0); a++`
const ast = parse(src, { sourceType: 'module' })
const s = new MagicString(src)
const {
rootRefs, // ['a']
importedHelpers // ['ref']
} = transformAST(ast, s)
console.log(s.toString()) // let a = _ref(0); a.value++
```

View File

@ -1,292 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`$ unwrapping 1`] = `
"
import { ref, shallowRef } from 'vue'
let foo = (ref())
export let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
label: var e = (ref())
"
`;
exports[`$$ 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
const b = (a)
const c = ({ a })
callExternal((a))
"
`;
exports[`$$ with some edge cases 1`] = `
"import { ref as _ref } from 'vue'
;( /* 2 */ count /* 2 */ )
;( count /* 2 */, /**/ a )
;( (count /* 2 */, /**/ a) /**/ )
{
a:(count,a)
}
;((count) + 1)
;([count])
; (count )
console.log(((a)))
;(a,b)
;(((a++,b)))
count = ( a++ ,b)
count = ()=>(a++,b)
let r1 = _ref(a, (a++,b))
let r2 = { a:(a++,b),b: (a) }
switch((c)){
case d:
;(a)
;((h,f))
break
}
((count++,(count),(count,a)))
"
`;
exports[`$computed declaration 1`] = `
"import { computed as _computed } from 'vue'
let a = _computed(() => 1)
"
`;
exports[`$ref & $shallowRef declarations 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
let foo = _ref()
export let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
label: var e = _ref()
"
`;
exports[`accessing ref binding 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
console.log(a.value)
function get() {
return a.value + 1
}
"
`;
exports[`array destructure 1`] = `
"import { ref as _ref, toRef as _toRef } from 'vue'
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[`handle TS casting syntax 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
console.log(a.value!)
console.log(a.value! + 1)
console.log(a.value as number)
console.log((a.value as number) + 1)
console.log(<number>a.value)
console.log(<number>a.value + 1)
console.log(a.value! + (a.value as number))
console.log(a.value! + <number>a.value)
console.log((a.value as number) + <number>a.value)
"
`;
exports[`macro import alias and removal 1`] = `
"import { ref as _ref, toRef as _toRef } from 'vue'
let a = _ref(1)
const __$temp_1 = (useMouse()),
x = _toRef(__$temp_1, 'x'),
y = _toRef(__$temp_1, 'y');
"
`;
exports[`mixing $ref & $computed declarations 1`] = `
"import { ref as _ref, computed as _computed } from 'vue'
let a = _ref(1), b = _computed(() => a.value + 1)
"
`;
exports[`multi $ref declarations 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
"
`;
exports[`mutating ref binding 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
let b = _ref({ count: 0 })
function inc() {
a.value++
a.value = a.value + 1
b.value.count++
b.value.count = b.value.count + 1
;({ a: a.value } = { a: 2 })
;[a.value] = [1]
}
"
`;
exports[`nested destructure 1`] = `
"import { toRef as _toRef } from 'vue'
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[`nested scopes 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(0)
let b = _ref(0)
let c = 0
a.value++ // outer a
b.value++ // outer b
c++ // outer c
let bar = _ref(0)
bar.value++ // outer bar
function foo({ a }) {
a++ // inner a
b.value++ // inner b
let c = _ref(0)
c.value++ // inner c
let d = _ref(0)
function bar(c) {
c++ // nested c
d.value++ // nested d
}
bar() // inner bar
if (true) {
let a = _ref(0)
a.value++ // if block a
}
return ({ a, b, c, d })
}
"
`;
exports[`object destructure 1`] = `
"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 = (useSomething(() => 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 overwrite current scope 1`] = `
"
const fn = () => {
const $ = () => 'foo'
const $ref = () => 'bar'
const $$ = () => 'baz'
console.log($())
console.log($ref())
console.log($$())
}
"
`;
exports[`should not overwrite importing 1`] = `
"
import { $, $$ } from './foo'
$('foo')
$$('bar')
"
`;
exports[`should not rewrite scope variable 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
let b = _ref(1)
let d = _ref(1)
const e = 1
function test() {
const a = 2
console.log(a)
console.log(b.value)
let c = { c: 3 }
console.log(c)
console.log(d.value)
console.log(e)
}
let err = _ref(null)
try {
} catch (err) {
console.log(err)
}
"
`;
exports[`should not rewrite type identifiers 1`] = `
"import { ref as _ref } from 'vue'
const props = defineProps<{msg: string; ids?: string[]}>()
let ids = _ref([])"
`;
exports[`using ref binding in property shorthand 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(1)
const b = { a: a.value }
function test() {
const { a } = b
}
"
`;

View File

@ -1,564 +0,0 @@
import { parse } from '@babel/parser'
import { transform } from '../src'
function assertCode(code: string) {
// parse the generated code to make sure it is valid
try {
parse(code, {
sourceType: 'module',
plugins: ['typescript']
})
} catch (e: any) {
console.log(code)
throw e
}
expect(code).toMatchSnapshot()
}
test('$ unwrapping', () => {
const { code, rootRefs } = transform(`
import { ref, shallowRef } from 'vue'
let foo = $(ref())
export let a = $(ref(1))
let b = $(shallowRef({
count: 0
}))
let c = () => {}
let d
label: var e = $(ref())
`)
expect(code).not.toMatch(`$(ref())`)
expect(code).not.toMatch(`$(ref(1))`)
expect(code).not.toMatch(`$(shallowRef({`)
expect(code).toMatch(`let foo = (ref())`)
expect(code).toMatch(`export let a = (ref(1))`)
expect(code).toMatch(`
let b = (shallowRef({
count: 0
}))
`)
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
expect(code).toMatch(`label: var e = (ref())`)
expect(rootRefs).toStrictEqual(['foo', 'a', 'b', 'e'])
assertCode(code)
})
test('$ref & $shallowRef declarations', () => {
const { code, rootRefs, importedHelpers } = transform(`
let foo = $ref()
export let a = $ref(1)
let b = $shallowRef({
count: 0
})
let c = () => {}
let d
label: var e = $ref()
`)
expect(code).toMatch(
`import { ref as _ref, shallowRef as _shallowRef } from 'vue'`
)
expect(code).not.toMatch(`$ref()`)
expect(code).not.toMatch(`$ref(1)`)
expect(code).not.toMatch(`$shallowRef({`)
expect(code).toMatch(`let foo = _ref()`)
expect(code).toMatch(`let a = _ref(1)`)
expect(code).toMatch(`
let b = _shallowRef({
count: 0
})
`)
// normal declarations left untouched
expect(code).toMatch(`let c = () => {}`)
expect(code).toMatch(`let d`)
expect(code).toMatch(`label: var e = _ref()`)
expect(rootRefs).toStrictEqual(['foo', 'a', 'b', 'e'])
expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
assertCode(code)
})
test('multi $ref declarations', () => {
const { code, rootRefs, importedHelpers } = transform(`
let a = $ref(1), b = $ref(2), c = $ref({
count: 0
})
`)
expect(code).toMatch(`
let a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
`)
expect(rootRefs).toStrictEqual(['a', 'b', 'c'])
expect(importedHelpers).toStrictEqual(['ref'])
assertCode(code)
})
test('$computed declaration', () => {
const { code, rootRefs, importedHelpers } = transform(`
let a = $computed(() => 1)
`)
expect(code).toMatch(`
let a = _computed(() => 1)
`)
expect(rootRefs).toStrictEqual(['a'])
expect(importedHelpers).toStrictEqual(['computed'])
assertCode(code)
})
test('mixing $ref & $computed declarations', () => {
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(rootRefs).toStrictEqual(['a', 'b'])
expect(importedHelpers).toStrictEqual(['ref', 'computed'])
assertCode(code)
})
test('accessing ref binding', () => {
const { code } = transform(`
let a = $ref(1)
console.log(a)
function get() {
return a + 1
}
`)
expect(code).toMatch(`console.log(a.value)`)
expect(code).toMatch(`return a.value + 1`)
assertCode(code)
})
describe('cases that should not append .value', () => {
test('member expression', () => {
const { code } = transform(`
let a = $ref(1)
console.log(b.a)
`)
expect(code).not.toMatch(`a.value`)
})
test('function argument', () => {
const { code } = transform(`
let a = $ref(1)
function get(a) {
return a + 1
}
function get2({ a }) {
return a + 1
}
function get3([a]) {
return a + 1
}
`)
expect(code).not.toMatch(`a.value`)
})
test('for in/of loops', () => {
const { code } = transform(`
let a = $ref(1)
for (const [a, b] of arr) {
console.log(a)
}
for (let a in arr) {
console.log(a)
}
`)
expect(code).not.toMatch(`a.value`)
})
})
test('mutating ref binding', () => {
const { code } = transform(`
let a = $ref(1)
let b = $ref({ count: 0 })
function inc() {
a++
a = a + 1
b.count++
b.count = b.count + 1
;({ a } = { a: 2 })
;[a] = [1]
}
`)
expect(code).toMatch(`a.value++`)
expect(code).toMatch(`a.value = a.value + 1`)
expect(code).toMatch(`b.value.count++`)
expect(code).toMatch(`b.value.count = b.value.count + 1`)
expect(code).toMatch(`;({ a: a.value } = { a: 2 })`)
expect(code).toMatch(`;[a.value] = [1]`)
assertCode(code)
})
test('using ref binding in property shorthand', () => {
const { code } = transform(`
let a = $ref(1)
const b = { a }
function test() {
const { a } = b
}
`)
expect(code).toMatch(`const b = { a: a.value }`)
// should not convert destructure
expect(code).toMatch(`const { a } = b`)
assertCode(code)
})
test('should not rewrite scope variable', () => {
const { code } = transform(`
let a = $ref(1)
let b = $ref(1)
let d = $ref(1)
const e = 1
function test() {
const a = 2
console.log(a)
console.log(b)
let c = { c: 3 }
console.log(c)
console.log(d)
console.log(e)
}
let err = $ref(null)
try {
} catch (err) {
console.log(err)
}
`)
expect(code).toMatch('console.log(a)')
expect(code).toMatch('console.log(b.value)')
expect(code).toMatch('console.log(c)')
expect(code).toMatch('console.log(d.value)')
expect(code).toMatch('console.log(e)')
expect(code).toMatch('console.log(err)')
assertCode(code)
})
test('object destructure', () => {
const { code, rootRefs } = transform(`
let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
let { foo } = $(useSomething(() => 1));
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(
`console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)`
)
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] = $(useFoo())
console.log(n, a, b)
`)
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)
})
test('nested destructure', () => {
const { code, rootRefs } = transform(`
let [{ a: { b }}] = $(useFoo())
let { c: [d, e] } = $(useBar())
console.log(b, d, 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)
})
test('$$', () => {
const { code } = transform(`
let a = $ref(1)
const b = $$(a)
const c = $$({ a })
callExternal($$(a))
`)
expect(code).toMatch(`const b = (a)`)
expect(code).toMatch(`const c = ({ a })`)
expect(code).toMatch(`callExternal((a))`)
assertCode(code)
})
test('$$ with some edge cases', () => {
const { code } = transform(`
$$( /* 2 */ count /* 2 */ )
$$( count /* 2 */, /**/ a )
$$( (count /* 2 */, /**/ a) /**/ )
{
a:$$(count,a)
}
$$((count) + 1)
$$([count])
$$ (count )
console.log($$($$(a)))
$$(a,b)
$$($$((a++,b)))
count = $$( a++ ,b)
count = ()=>$$(a++,b)
let r1 = $ref(a, $$(a++,b))
let r2 = { a:$$(a++,b),b:$$ (a) }
switch($$(c)){
case d:
$$(a)
$$($$(h,f))
break
}
($$(count++,$$(count),$$(count,a)))
`)
expect(code).toMatch(`/* 2 */ count /* 2 */`)
expect(code).toMatch(`;( count /* 2 */, /**/ a )`)
expect(code).toMatch(`;( (count /* 2 */, /**/ a) /**/ )`)
expect(code).toMatch(`a:(count,a)`)
expect(code).toMatch(`;((count) + 1)`)
expect(code).toMatch(`;([count])`)
expect(code).toMatch(`;(a,b)`)
expect(code).toMatch(`log(((a)))`)
expect(code).toMatch(`count = ( a++ ,b)`)
expect(code).toMatch(`()=>(a++,b)`)
expect(code).toMatch(`_ref(a, (a++,b))`)
expect(code).toMatch(`{ a:(a++,b),b: (a) }`)
expect(code).toMatch(`switch((c))`)
expect(code).toMatch(`;((h,f))`)
expect(code).toMatch(`((count++,(count),(count,a)))`)
assertCode(code)
})
test('nested scopes', () => {
const { code, rootRefs } = transform(`
let a = $ref(0)
let b = $ref(0)
let c = 0
a++ // outer a
b++ // outer b
c++ // outer c
let bar = $ref(0)
bar++ // outer bar
function foo({ a }) {
a++ // inner a
b++ // inner b
let c = $ref(0)
c++ // inner c
let d = $ref(0)
function bar(c) {
c++ // nested c
d++ // nested d
}
bar() // inner bar
if (true) {
let a = $ref(0)
a++ // if block a
}
return $$({ a, b, c, d })
}
`)
expect(rootRefs).toStrictEqual(['a', 'b', 'bar'])
expect(code).toMatch('a.value++ // outer a')
expect(code).toMatch('b.value++ // outer b')
expect(code).toMatch('c++ // outer c')
expect(code).toMatch('a++ // inner a') // shadowed by function arg
expect(code).toMatch('b.value++ // inner b')
expect(code).toMatch('c.value++ // inner c') // shadowed by local ref binding
expect(code).toMatch('c++ // nested c') // shadowed by inline fn arg
expect(code).toMatch(`d.value++ // nested d`)
expect(code).toMatch(`a.value++ // if block a`) // if block
expect(code).toMatch(`bar.value++ // outer bar`)
// inner bar shadowed by function declaration
expect(code).toMatch(`bar() // inner bar`)
expect(code).toMatch(`return ({ a, b, c, d })`)
assertCode(code)
})
//#4062
test('should not rewrite type identifiers', () => {
const { code } = transform(
`const props = defineProps<{msg: string; ids?: string[]}>()
let ids = $ref([])`,
{
parserPlugins: ['typescript']
}
)
expect(code).not.toMatch('.value')
assertCode(code)
})
// #4254
test('handle TS casting syntax', () => {
const { code } = transform(
`
let a = $ref(1)
console.log(a!)
console.log(a! + 1)
console.log(a as number)
console.log((a as number) + 1)
console.log(<number>a)
console.log(<number>a + 1)
console.log(a! + (a as number))
console.log(a! + <number>a)
console.log((a as number) + <number>a)
`,
{
parserPlugins: ['typescript']
}
)
expect(code).toMatch('console.log(a.value!)')
expect(code).toMatch('console.log(a.value as number)')
expect(code).toMatch('console.log(<number>a.value)')
assertCode(code)
})
test('macro import alias and removal', () => {
const { code } = transform(
`
import { $ as fromRefs, $ref } from 'vue/macros'
let a = $ref(1)
const { x, y } = fromRefs(useMouse())
`
)
// should remove imports
expect(code).not.toMatch(`from 'vue/macros'`)
expect(code).toMatch(`let a = _ref(1)`)
expect(code).toMatch(`const __$temp_1 = (useMouse())`)
assertCode(code)
})
// #6838
test('should not overwrite importing', () => {
const { code } = transform(
`
import { $, $$ } from './foo'
$('foo')
$$('bar')
`
)
assertCode(code)
})
// #6838
test('should not overwrite current scope', () => {
const { code } = transform(
`
const fn = () => {
const $ = () => 'foo'
const $ref = () => 'bar'
const $$ = () => 'baz'
console.log($())
console.log($ref())
console.log($$())
}
`
)
assertCode(code)
})
describe('errors', () => {
test('$ref w/ destructure', () => {
expect(() => transform(`let { a } = $ref(1)`)).toThrow(
`cannot be used with destructure`
)
})
test('$computed w/ destructure', () => {
expect(() => transform(`let { a } = $computed(() => 1)`)).toThrow(
`cannot be used with destructure`
)
})
test('warn usage in non-init positions', () => {
expect(() =>
transform(
`let bar = $ref(1)
bar = $ref(2)`
)
).toThrow(`$ref can only be used as the initializer`)
expect(() => transform(`let bar = { foo: $computed(1) }`)).toThrow(
`$computed can only be used as the initializer`
)
})
test('not transform the prototype attributes', () => {
const { code } = transform(`
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (val, key) => hasOwnProperty.call(val, key)
`)
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`
)
})
test('assignment to constant variable', () => {
expect(() =>
transform(`
const foo = $ref(0)
foo = 1
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const [a, b] = $([1, 2])
a = 1
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const foo = $ref(0)
foo++
`)
).toThrow(`Assignment to constant variable.`)
expect(() =>
transform(`
const foo = $ref(0)
bar = foo
`)
).not.toThrow()
})
})

View File

@ -1,41 +0,0 @@
{
"name": "@vue/reactivity-transform",
"version": "3.4.0-alpha.1",
"description": "@vue/reactivity-transform",
"main": "dist/reactivity-transform.cjs.js",
"files": [
"dist"
],
"buildOptions": {
"formats": [
"cjs"
],
"prod": false
},
"types": "dist/reactivity-transform.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core.git",
"directory": "packages/reactivity-transform"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core/issues"
},
"homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
"dependencies": {
"@babel/parser": "^7.23.3",
"@vue/compiler-core": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
},
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/types": "^7.23.3"
}
}

View File

@ -1,3 +0,0 @@
export function plugin() {
// TODO
}

View File

@ -1 +0,0 @@
export * from './reactivityTransform'

View File

@ -1,794 +0,0 @@
import {
Node,
Identifier,
BlockStatement,
CallExpression,
ObjectPattern,
ArrayPattern,
Program,
VariableDeclarator,
Expression,
VariableDeclaration,
ImportDeclaration,
ImportSpecifier,
ImportDefaultSpecifier,
ImportNamespaceSpecifier
} from '@babel/types'
import MagicString, { SourceMap } from 'magic-string'
import { walk } from 'estree-walker'
import {
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
isReferencedIdentifier,
isStaticProperty,
walkFunctionParams
} from '@vue/compiler-core'
import { parse, ParserPlugin } from '@babel/parser'
import { hasOwn, isArray, isString, genPropsAccessExp } from '@vue/shared'
const CONVERT_SYMBOL = '$'
const ESCAPE_SYMBOL = '$$'
const IMPORT_SOURCE = 'vue/macros'
const shorthands = ['ref', 'computed', 'shallowRef', 'toRef', 'customRef']
const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\s*(\(|\<)/
/**
* @deprecated will be removed in 3.4
*/
export function shouldTransform(src: string): boolean {
return transformCheckRE.test(src)
}
interface Binding {
isConst?: boolean
isProp?: boolean
}
type Scope = Record<string, Binding | false>
export interface RefTransformOptions {
filename?: string
sourceMap?: boolean
parserPlugins?: ParserPlugin[]
importHelpersFrom?: string
}
export interface RefTransformResults {
code: string
map: SourceMap | null
rootRefs: string[]
importedHelpers: string[]
}
export interface ImportBinding {
local: string
imported: string
source: string
specifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier
}
/**
* @deprecated will be removed in 3.4
*/
export function transform(
src: string,
{
filename,
sourceMap,
parserPlugins,
importHelpersFrom = 'vue'
}: RefTransformOptions = {}
): RefTransformResults {
const plugins: ParserPlugin[] = parserPlugins || []
if (filename) {
if (/\.tsx?$/.test(filename)) {
plugins.push('typescript')
}
if (filename.endsWith('x')) {
plugins.push('jsx')
}
}
const ast = parse(src, {
sourceType: 'module',
plugins
})
const s = new MagicString(src)
const res = transformAST(ast.program, s, 0)
// inject helper imports
if (res.importedHelpers.length) {
s.prepend(
`import { ${res.importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from '${importHelpersFrom}'\n`
)
}
return {
...res,
code: s.toString(),
map: sourceMap
? s.generateMap({
source: filename,
hires: true,
includeContent: true
})
: null
}
}
/**
* @deprecated will be removed in 3.4
*/
export function transformAST(
ast: Program,
s: MagicString,
offset = 0,
knownRefs?: string[],
knownProps?: Record<
string, // public prop key
{
local: string // local identifier, may be different
default?: any
isConst?: boolean
}
>
): {
rootRefs: string[]
importedHelpers: string[]
} {
warnExperimental()
const userImports: Record<string, ImportBinding> = Object.create(null)
for (const node of ast.body) {
if (node.type !== 'ImportDeclaration') continue
walkImportDeclaration(node)
}
// macro import handling
let convertSymbol: string | undefined
let escapeSymbol: string | undefined
for (const { local, imported, source, specifier } of Object.values(
userImports
)) {
if (source === IMPORT_SOURCE) {
if (imported === ESCAPE_SYMBOL) {
escapeSymbol = local
} else if (imported === CONVERT_SYMBOL) {
convertSymbol = local
} else if (imported !== local) {
error(
`macro imports for ref-creating methods do not support aliasing.`,
specifier
)
}
}
}
// default symbol
if (!convertSymbol && !userImports[CONVERT_SYMBOL]) {
convertSymbol = CONVERT_SYMBOL
}
if (!escapeSymbol && !userImports[ESCAPE_SYMBOL]) {
escapeSymbol = ESCAPE_SYMBOL
}
const importedHelpers = new Set<string>()
const rootScope: Scope = {}
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
let escapeScope: CallExpression | undefined // inside $$()
const excludedIds = new WeakSet<Identifier>()
const parentStack: Node[] = []
const propsLocalToPublicMap: Record<string, string> = Object.create(null)
if (knownRefs) {
for (const key of knownRefs) {
rootScope[key] = {}
}
}
if (knownProps) {
for (const key in knownProps) {
const { local, isConst } = knownProps[key]
rootScope[local] = {
isProp: true,
isConst: !!isConst
}
propsLocalToPublicMap[local] = key
}
}
function walkImportDeclaration(node: ImportDeclaration) {
const source = node.source.value
if (source === IMPORT_SOURCE) {
s.remove(node.start! + offset, node.end! + offset)
}
for (const specifier of node.specifiers) {
const local = specifier.local.name
const imported =
(specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name) ||
'default'
userImports[local] = {
source,
local,
imported,
specifier
}
}
}
function isRefCreationCall(callee: string): string | false {
if (!convertSymbol || currentScope[convertSymbol] !== undefined) {
return false
}
if (callee === convertSymbol) {
return convertSymbol
}
if (callee[0] === '$' && shorthands.includes(callee.slice(1))) {
return callee
}
return false
}
function error(msg: string, node: Node): never {
const e = new Error(msg)
;(e as any).node = node
throw e
}
function helper(msg: string) {
importedHelpers.add(msg)
return `_${msg}`
}
function registerBinding(id: Identifier, binding?: Binding) {
excludedIds.add(id)
if (currentScope) {
currentScope[id.name] = binding ? binding : false
} else {
error(
'registerBinding called without active scope, something is wrong.',
id
)
}
}
const registerRefBinding = (id: Identifier, isConst = false) =>
registerBinding(id, { isConst })
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') {
walkVariableDeclaration(stmt, isRoot)
} else if (
stmt.type === 'FunctionDeclaration' ||
stmt.type === 'ClassDeclaration'
) {
if (stmt.declare || !stmt.id) continue
registerBinding(stmt.id)
} else if (
(stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
stmt.left.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.left)
} else if (
stmt.type === 'ExportNamedDeclaration' &&
stmt.declaration &&
stmt.declaration.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.declaration, isRoot)
} else if (
stmt.type === 'LabeledStatement' &&
stmt.body.type === 'VariableDeclaration'
) {
walkVariableDeclaration(stmt.body, isRoot)
}
}
}
function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
if (stmt.declare) {
return
}
for (const decl of stmt.declarations) {
let refCall
const isCall =
decl.init &&
decl.init.type === 'CallExpression' &&
decl.init.callee.type === 'Identifier'
if (
isCall &&
(refCall = isRefCreationCall((decl as any).init.callee.name))
) {
processRefDeclaration(
refCall,
decl.id,
decl.init as CallExpression,
stmt.kind === 'const'
)
} else {
const isProps =
isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
for (const id of extractIdentifiers(decl.id)) {
if (isProps) {
// for defineProps destructure, only exclude them since they
// are already passed in as knownProps
excludedIds.add(id)
} else {
registerBinding(id)
}
}
}
}
}
function processRefDeclaration(
method: string,
id: VariableDeclarator['id'],
call: CallExpression,
isConst: boolean
) {
excludedIds.add(call.callee as Identifier)
if (method === convertSymbol) {
// $
// remove macro
s.remove(call.callee.start! + offset, call.callee.end! + offset)
if (id.type === 'Identifier') {
// single variable
registerRefBinding(id, isConst)
} else if (id.type === 'ObjectPattern') {
processRefObjectPattern(id, call, isConst)
} else if (id.type === 'ArrayPattern') {
processRefArrayPattern(id, call, isConst)
}
} else {
// shorthands
if (id.type === 'Identifier') {
registerRefBinding(id, isConst)
// replace call
s.overwrite(
call.start! + offset,
call.start! + method.length + offset,
helper(method.slice(1))
)
} else {
error(`${method}() cannot be used with destructure patterns.`, call)
}
}
}
function processRefObjectPattern(
pattern: ObjectPattern,
call: CallExpression,
isConst: boolean,
tempVar?: string,
path: PathSegment[] = []
) {
if (!tempVar) {
tempVar = genTempVar()
// const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
let nameId: Identifier | undefined
for (const p of pattern.properties) {
let key: Expression | string | undefined
let defaultValue: Expression | undefined
if (p.type === 'ObjectProperty') {
if (p.key.start! === p.value.start!) {
// shorthand { foo }
nameId = p.key as Identifier
if (p.value.type === 'Identifier') {
// avoid shorthand value identifier from being processed
excludedIds.add(p.value)
} else if (
p.value.type === 'AssignmentPattern' &&
p.value.left.type === 'Identifier'
) {
// { foo = 1 }
excludedIds.add(p.value.left)
defaultValue = p.value.right
}
} else {
key = p.computed ? (p.key as Expression) : (p.key as Identifier).name
if (p.value.type === 'Identifier') {
// { foo: bar }
nameId = p.value
} else if (p.value.type === 'ObjectPattern') {
processRefObjectPattern(p.value, call, isConst, tempVar, [
...path,
key
])
} else if (p.value.type === 'ArrayPattern') {
processRefArrayPattern(p.value, call, isConst, tempVar, [
...path,
key
])
} else if (p.value.type === 'AssignmentPattern') {
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, isConst, tempVar, [
...path,
[key, p.value.right]
])
} else if (p.value.left.type === 'ArrayPattern') {
processRefArrayPattern(p.value.left, call, isConst, tempVar, [
...path,
[key, p.value.right]
])
} else {
// MemberExpression case is not possible here, ignore
}
}
}
} else {
// rest element { ...foo }
error(`reactivity destructure does not support rest elements.`, p)
}
if (nameId) {
registerRefBinding(nameId, isConst)
// 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(
call.end! + offset,
`,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${keyStr}${defaultStr})`
)
}
}
if (nameId) {
s.appendLeft(call.end! + offset, ';')
}
}
function processRefArrayPattern(
pattern: ArrayPattern,
call: CallExpression,
isConst: boolean,
tempVar?: string,
path: PathSegment[] = []
) {
if (!tempVar) {
// const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
tempVar = genTempVar()
s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
}
let nameId: Identifier | undefined
for (let i = 0; i < pattern.elements.length; i++) {
const e = pattern.elements[i]
if (!e) continue
let defaultValue: Expression | undefined
if (e.type === 'Identifier') {
// [a] --> [__a]
nameId = e
} else if (e.type === 'AssignmentPattern') {
// [a = 1]
nameId = e.left as Identifier
defaultValue = e.right
} else if (e.type === 'RestElement') {
// [...a]
error(`reactivity destructure does not support rest elements.`, e)
} else if (e.type === 'ObjectPattern') {
processRefObjectPattern(e, call, isConst, tempVar, [...path, i])
} else if (e.type === 'ArrayPattern') {
processRefArrayPattern(e, call, isConst, tempVar, [...path, i])
}
if (nameId) {
registerRefBinding(nameId, isConst)
// inject toRef() after original replaced pattern
const source = pathToString(tempVar, path)
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft(
call.end! + offset,
`,\n ${nameId.name} = ${helper(
'toRef'
)}(${source}, ${i}${defaultStr})`
)
}
}
if (nameId) {
s.appendLeft(call.end! + offset, ';')
}
}
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,
parent: Node,
parentStack: Node[]
): boolean {
if (hasOwn(scope, id.name)) {
const binding = scope[id.name]
if (binding) {
if (
binding.isConst &&
((parent.type === 'AssignmentExpression' && id === parent.left) ||
parent.type === 'UpdateExpression')
) {
error(`Assignment to constant variable.`, id)
}
const { isProp } = binding
if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
if (isProp) {
if (escapeScope) {
// prop binding in $$()
// { prop } -> { prop: __props_prop }
registerEscapedPropBinding(id)
s.appendLeft(
id.end! + offset,
`: __props_${propsLocalToPublicMap[id.name]}`
)
} else {
// { prop } -> { prop: __props.prop }
s.appendLeft(
id.end! + offset,
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
)
}
} else {
// { foo } -> { foo: foo.value }
s.appendLeft(id.end! + offset, `: ${id.name}.value`)
}
}
} else {
if (isProp) {
if (escapeScope) {
// x --> __props_x
registerEscapedPropBinding(id)
s.overwrite(
id.start! + offset,
id.end! + offset,
`__props_${propsLocalToPublicMap[id.name]}`
)
} else {
// x --> __props.x
s.overwrite(
id.start! + offset,
id.end! + offset,
genPropsAccessExp(propsLocalToPublicMap[id.name])
)
}
} else {
// x --> x.value
s.appendLeft(id.end! + offset, '.value')
}
}
}
return true
}
return false
}
const propBindingRefs: Record<string, true> = {}
function registerEscapedPropBinding(id: Identifier) {
if (!propBindingRefs.hasOwnProperty(id.name)) {
propBindingRefs[id.name] = true
const publicKey = propsLocalToPublicMap[id.name]
s.prependRight(
offset,
`const __props_${publicKey} = ${helper(
`toRef`
)}(__props, '${publicKey}');\n`
)
}
}
// check root scope first
walkScope(ast, true)
walk(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)
// function scopes
if (isFunctionType(node)) {
scopeStack.push((currentScope = {}))
walkFunctionParams(node, registerBinding)
if (node.body.type === 'BlockStatement') {
walkScope(node.body)
}
return
}
// catch param
if (node.type === 'CatchClause') {
scopeStack.push((currentScope = {}))
if (node.param && node.param.type === 'Identifier') {
registerBinding(node.param)
}
walkScope(node.body)
return
}
// non-function block scopes
if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
scopeStack.push((currentScope = {}))
walkScope(node)
return
}
// skip type nodes
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
) {
return this.skip()
}
if (node.type === 'Identifier') {
const binding = rootScope[node.name]
if (
// if inside $$(), skip unless this is a destructured prop binding
!(escapeScope && (!binding || !binding.isProp)) &&
isReferencedIdentifier(node, parent!, parentStack) &&
!excludedIds.has(node)
) {
// walk up the scope chain to check if id should be appended .value
let i = scopeStack.length
while (i--) {
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
return
}
}
}
}
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
const callee = node.callee.name
const refCall = isRefCreationCall(callee)
if (refCall && (!parent || parent.type !== 'VariableDeclarator')) {
return error(
`${refCall} can only be used as the initializer of ` +
`a variable declaration.`,
node
)
}
if (
escapeSymbol &&
currentScope[escapeSymbol] === undefined &&
callee === escapeSymbol
) {
escapeScope = node
s.remove(node.callee.start! + offset, node.callee.end! + offset)
if (parent?.type === 'ExpressionStatement') {
// edge case where the call expression is an expression statement
// if its own - prepend semicolon to avoid it being parsed as
// function invocation of previous line
let i =
(node.leadingComments
? node.leadingComments[0].start
: node.start)! + offset
while (i--) {
const char = s.original.charAt(i)
if (char === '\n') {
// only insert semi if it's actually the first thing after
// newline
s.prependRight(node.start! + offset, ';')
break
} else if (!/\s/.test(char)) {
break
}
}
}
}
}
},
leave(node: Node, parent?: Node) {
parent && parentStack.pop()
if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
isFunctionType(node)
) {
scopeStack.pop()
currentScope = scopeStack[scopeStack.length - 1] || null
}
if (node === escapeScope) {
escapeScope = undefined
}
}
})
return {
rootRefs: Object.keys(rootScope).filter(key => {
const binding = rootScope[key]
return binding && !binding.isProp
}),
importedHelpers: [...importedHelpers]
}
}
const hasWarned: Record<string, boolean> = {}
function warnExperimental() {
// eslint-disable-next-line
if (typeof window !== 'undefined') {
return
}
warnOnce(
`Reactivity Transform was an experimental feature and has now been deprecated. ` +
`It will be removed from Vue core in 3.4. If you intend to continue using it, ` +
`switch to https://vue-macros.sxzz.moe/features/reactivity-transform.html.\n` +
`See reason for deprecation here: https://github.com/vuejs/rfcs/discussions/369#discussioncomment-5059028`
)
}
function warnOnce(msg: string) {
const isNodeProd =
typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
if (!isNodeProd && !__TEST__ && !hasWarned[msg]) {
hasWarned[msg] = true
warn(msg)
}
}
function warn(msg: string) {
console.warn(
`\x1b[1m\x1b[33m[@vue/reactivity-transform]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`
)
}

View File

@ -42,7 +42,7 @@ const sfcOptions: SFCOptions = {
script: {
inlineTemplate: !useDevMode.value,
isProd: !useDevMode.value,
reactivityTransform: true,
propsDestructure: true,
defineModel: true
},
style: {

View File

@ -1,19 +0,0 @@
import {
$ as _$,
$$ as _$$,
$ref as _$ref,
$shallowRef as _$shallowRef,
$computed as _$computed,
$customRef as _$customRef,
$toRef as _$toRef
} from './macros'
declare global {
const $: typeof _$
const $$: typeof _$$
const $ref: typeof _$ref
const $shallowRef: typeof _$shallowRef
const $computed: typeof _$computed
const $customRef: typeof _$customRef
const $toRef: typeof _$toRef
}

View File

@ -14,10 +14,7 @@
"compiler-sfc",
"server-renderer",
"jsx-runtime",
"jsx.d.ts",
"macros.d.ts",
"macros-global.d.ts",
"ref-macros.d.ts"
"jsx.d.ts"
],
"exports": {
".": {
@ -65,10 +62,7 @@
},
"./jsx": "./jsx.d.ts",
"./dist/*": "./dist/*",
"./package.json": "./package.json",
"./macros": "./macros.d.ts",
"./macros-global": "./macros-global.d.ts",
"./ref-macros": "./ref-macros.d.ts"
"./package.json": "./package.json"
},
"buildOptions": {
"name": "Vue",

View File

@ -1,2 +0,0 @@
// TODO remove in 3.4
import './macros-global'

View File

@ -198,9 +198,6 @@ importers:
'@vue/compiler-ssr':
specifier: workspace:*
version: link:../compiler-ssr
'@vue/reactivity-transform':
specifier: workspace:*
version: link:../reactivity-transform
'@vue/shared':
specifier: workspace:*
version: link:../shared
@ -284,31 +281,6 @@ importers:
specifier: workspace:*
version: link:../shared
packages/reactivity-transform:
dependencies:
'@babel/parser':
specifier: ^7.23.3
version: 7.23.3
'@vue/compiler-core':
specifier: workspace:*
version: link:../compiler-core
'@vue/shared':
specifier: workspace:*
version: link:../shared
estree-walker:
specifier: ^2.0.2
version: 2.0.2
magic-string:
specifier: ^0.30.5
version: 0.30.5
devDependencies:
'@babel/core':
specifier: ^7.23.3
version: 7.23.3
'@babel/types':
specifier: ^7.23.3
version: 7.23.3
packages/runtime-core:
dependencies:
'@vue/reactivity':

View File

@ -6,7 +6,6 @@ const packagesToCheck = [
'compiler-core',
'compiler-dom',
'compiler-ssr',
'reactivity-transform',
'shared'
]