feat(compiler-sfc): <script setup> defineProps destructure transform (#4690)
This commit is contained in:
@@ -64,7 +64,9 @@ const {
|
||||
// @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: [/* ... */]
|
||||
parserPlugins: [
|
||||
/* ... */
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
@@ -93,7 +95,7 @@ const ast = parse(src, { sourceType: 'module' })
|
||||
const s = new MagicString(src)
|
||||
|
||||
const {
|
||||
rootVars, // ['a']
|
||||
rootRefs, // ['a']
|
||||
importedHelpers // ['ref']
|
||||
} = transformAST(ast, s)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function assertCode(code: string) {
|
||||
}
|
||||
|
||||
test('$ unwrapping', () => {
|
||||
const { code, rootVars } = transform(`
|
||||
const { code, rootRefs } = transform(`
|
||||
import { ref, shallowRef } from 'vue'
|
||||
let foo = $(ref())
|
||||
let a = $(ref(1))
|
||||
@@ -40,12 +40,12 @@ test('$ unwrapping', () => {
|
||||
// normal declarations left untouched
|
||||
expect(code).toMatch(`let c = () => {}`)
|
||||
expect(code).toMatch(`let d`)
|
||||
expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
|
||||
expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('$ref & $shallowRef declarations', () => {
|
||||
const { code, rootVars, importedHelpers } = transform(`
|
||||
const { code, rootRefs, importedHelpers } = transform(`
|
||||
let foo = $ref()
|
||||
let a = $ref(1)
|
||||
let b = $shallowRef({
|
||||
@@ -70,13 +70,13 @@ test('$ref & $shallowRef declarations', () => {
|
||||
// normal declarations left untouched
|
||||
expect(code).toMatch(`let c = () => {}`)
|
||||
expect(code).toMatch(`let d`)
|
||||
expect(rootVars).toStrictEqual(['foo', 'a', 'b'])
|
||||
expect(rootRefs).toStrictEqual(['foo', 'a', 'b'])
|
||||
expect(importedHelpers).toStrictEqual(['ref', 'shallowRef'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('multi $ref declarations', () => {
|
||||
const { code, rootVars, importedHelpers } = transform(`
|
||||
const { code, rootRefs, importedHelpers } = transform(`
|
||||
let a = $ref(1), b = $ref(2), c = $ref({
|
||||
count: 0
|
||||
})
|
||||
@@ -86,31 +86,31 @@ test('multi $ref declarations', () => {
|
||||
count: 0
|
||||
})
|
||||
`)
|
||||
expect(rootVars).toStrictEqual(['a', 'b', 'c'])
|
||||
expect(rootRefs).toStrictEqual(['a', 'b', 'c'])
|
||||
expect(importedHelpers).toStrictEqual(['ref'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('$computed declaration', () => {
|
||||
const { code, rootVars, importedHelpers } = transform(`
|
||||
const { code, rootRefs, importedHelpers } = transform(`
|
||||
let a = $computed(() => 1)
|
||||
`)
|
||||
expect(code).toMatch(`
|
||||
let a = _computed(() => 1)
|
||||
`)
|
||||
expect(rootVars).toStrictEqual(['a'])
|
||||
expect(rootRefs).toStrictEqual(['a'])
|
||||
expect(importedHelpers).toStrictEqual(['computed'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('mixing $ref & $computed declarations', () => {
|
||||
const { code, rootVars, importedHelpers } = transform(`
|
||||
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(rootVars).toStrictEqual(['a', 'b'])
|
||||
expect(rootRefs).toStrictEqual(['a', 'b'])
|
||||
expect(importedHelpers).toStrictEqual(['ref', 'computed'])
|
||||
assertCode(code)
|
||||
})
|
||||
@@ -201,7 +201,7 @@ test('should not rewrite scope variable', () => {
|
||||
})
|
||||
|
||||
test('object destructure', () => {
|
||||
const { code, rootVars } = transform(`
|
||||
const { code, rootRefs } = transform(`
|
||||
let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo())
|
||||
let { foo } = $(useSomthing(() => 1));
|
||||
console.log(n, a, c, d, f, g, foo)
|
||||
@@ -221,12 +221,12 @@ test('object destructure', () => {
|
||||
expect(code).toMatch(
|
||||
`console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)`
|
||||
)
|
||||
expect(rootVars).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
|
||||
expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('array destructure', () => {
|
||||
const { code, rootVars } = transform(`
|
||||
const { code, rootRefs } = transform(`
|
||||
let n = $ref(1), [a, b = 1, ...c] = $(useFoo())
|
||||
console.log(n, a, b, c)
|
||||
`)
|
||||
@@ -235,12 +235,12 @@ test('array destructure', () => {
|
||||
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(rootVars).toStrictEqual(['n', 'a', 'b', 'c'])
|
||||
expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
test('nested destructure', () => {
|
||||
const { code, rootVars } = transform(`
|
||||
const { code, rootRefs } = transform(`
|
||||
let [{ a: { b }}] = $(useFoo())
|
||||
let { c: [d, e] } = $(useBar())
|
||||
console.log(b, d, e)
|
||||
@@ -252,7 +252,7 @@ test('nested destructure', () => {
|
||||
expect(code).toMatch(`\nconst b = _shallowRef(__b);`)
|
||||
expect(code).toMatch(`\nconst d = _shallowRef(__d);`)
|
||||
expect(code).toMatch(`\nconst e = _shallowRef(__e);`)
|
||||
expect(rootVars).toStrictEqual(['b', 'd', 'e'])
|
||||
expect(rootRefs).toStrictEqual(['b', 'd', 'e'])
|
||||
assertCode(code)
|
||||
})
|
||||
|
||||
@@ -270,7 +270,7 @@ test('$$', () => {
|
||||
})
|
||||
|
||||
test('nested scopes', () => {
|
||||
const { code, rootVars } = transform(`
|
||||
const { code, rootRefs } = transform(`
|
||||
let a = $ref(0)
|
||||
let b = $ref(0)
|
||||
let c = 0
|
||||
@@ -303,7 +303,7 @@ test('nested scopes', () => {
|
||||
return $$({ a, b, c, d })
|
||||
}
|
||||
`)
|
||||
expect(rootVars).toStrictEqual(['a', 'b', 'bar'])
|
||||
expect(rootRefs).toStrictEqual(['a', 'b', 'bar'])
|
||||
|
||||
expect(code).toMatch('a.value++ // outer a')
|
||||
expect(code).toMatch('b.value++ // outer b')
|
||||
|
||||
@@ -31,7 +31,7 @@ export function shouldTransform(src: string): boolean {
|
||||
return transformCheckRE.test(src)
|
||||
}
|
||||
|
||||
type Scope = Record<string, boolean>
|
||||
type Scope = Record<string, boolean | 'prop'>
|
||||
|
||||
export interface RefTransformOptions {
|
||||
filename?: string
|
||||
@@ -43,7 +43,7 @@ export interface RefTransformOptions {
|
||||
export interface RefTransformResults {
|
||||
code: string
|
||||
map: SourceMap | null
|
||||
rootVars: string[]
|
||||
rootRefs: string[]
|
||||
importedHelpers: string[]
|
||||
}
|
||||
|
||||
@@ -99,13 +99,23 @@ export function transformAST(
|
||||
ast: Program,
|
||||
s: MagicString,
|
||||
offset = 0,
|
||||
knownRootVars?: string[]
|
||||
knownRefs?: string[],
|
||||
knownProps?: Record<
|
||||
string, // public prop key
|
||||
{
|
||||
local: string // local identifier, may be different
|
||||
default?: any
|
||||
}
|
||||
>,
|
||||
rewritePropsOnly = false
|
||||
): {
|
||||
rootVars: string[]
|
||||
rootRefs: string[]
|
||||
importedHelpers: string[]
|
||||
} {
|
||||
// TODO remove when out of experimental
|
||||
warnExperimental()
|
||||
if (!rewritePropsOnly) {
|
||||
warnExperimental()
|
||||
}
|
||||
|
||||
const importedHelpers = new Set<string>()
|
||||
const rootScope: Scope = {}
|
||||
@@ -113,14 +123,23 @@ export function transformAST(
|
||||
let currentScope: Scope = rootScope
|
||||
const excludedIds = new WeakSet<Identifier>()
|
||||
const parentStack: Node[] = []
|
||||
const propsLocalToPublicMap = Object.create(null)
|
||||
|
||||
if (knownRootVars) {
|
||||
for (const key of knownRootVars) {
|
||||
if (knownRefs) {
|
||||
for (const key of knownRefs) {
|
||||
rootScope[key] = true
|
||||
}
|
||||
}
|
||||
if (knownProps) {
|
||||
for (const key in knownProps) {
|
||||
const { local } = knownProps[key]
|
||||
rootScope[local] = 'prop'
|
||||
propsLocalToPublicMap[local] = key
|
||||
}
|
||||
}
|
||||
|
||||
function error(msg: string, node: Node) {
|
||||
if (rewritePropsOnly) return
|
||||
const e = new Error(msg)
|
||||
;(e as any).node = node
|
||||
throw e
|
||||
@@ -145,17 +164,19 @@ export function transformAST(
|
||||
|
||||
const registerRefBinding = (id: Identifier) => registerBinding(id, true)
|
||||
|
||||
function walkScope(node: Program | BlockStatement) {
|
||||
function walkScope(node: Program | BlockStatement, isRoot = false) {
|
||||
for (const stmt of node.body) {
|
||||
if (stmt.type === 'VariableDeclaration') {
|
||||
if (stmt.declare) continue
|
||||
for (const decl of stmt.declarations) {
|
||||
let toVarCall
|
||||
if (
|
||||
const isCall =
|
||||
decl.init &&
|
||||
decl.init.type === 'CallExpression' &&
|
||||
decl.init.callee.type === 'Identifier' &&
|
||||
(toVarCall = isToVarCall(decl.init.callee.name))
|
||||
decl.init.callee.type === 'Identifier'
|
||||
if (
|
||||
isCall &&
|
||||
(toVarCall = isToVarCall((decl as any).init.callee.name))
|
||||
) {
|
||||
processRefDeclaration(
|
||||
toVarCall,
|
||||
@@ -164,8 +185,18 @@ export function transformAST(
|
||||
stmt
|
||||
)
|
||||
} else {
|
||||
const isProps =
|
||||
isRoot &&
|
||||
isCall &&
|
||||
(decl as any).init.callee.name === 'defineProps'
|
||||
for (const id of extractIdentifiers(decl.id)) {
|
||||
registerBinding(id)
|
||||
if (isProps) {
|
||||
// for defineProps destructure, only exclude them since they
|
||||
// are already passed in as knownProps
|
||||
excludedIds.add(id)
|
||||
} else {
|
||||
registerBinding(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,26 +334,48 @@ export function transformAST(
|
||||
}
|
||||
}
|
||||
|
||||
function checkRefId(
|
||||
function rewriteId(
|
||||
scope: Scope,
|
||||
id: Identifier,
|
||||
parent: Node,
|
||||
parentStack: Node[]
|
||||
): boolean {
|
||||
if (hasOwn(scope, id.name)) {
|
||||
if (scope[id.name]) {
|
||||
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 }
|
||||
// { prop } -> { prop: __prop.prop }
|
||||
// skip for destructure patterns
|
||||
if (
|
||||
!(parent as any).inPattern ||
|
||||
isInDestructureAssignment(parent, parentStack)
|
||||
) {
|
||||
s.appendLeft(id.end! + offset, `: ${id.name}.value`)
|
||||
if (isProp) {
|
||||
s.appendLeft(
|
||||
id.end! + offset,
|
||||
`: __props.${propsLocalToPublicMap[id.name]}`
|
||||
)
|
||||
} else {
|
||||
s.appendLeft(id.end! + offset, `: ${id.name}.value`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.appendLeft(id.end! + offset, '.value')
|
||||
if (isProp) {
|
||||
s.overwrite(
|
||||
id.start! + offset,
|
||||
id.end! + offset,
|
||||
`__props.${propsLocalToPublicMap[id.name]}`
|
||||
)
|
||||
} else {
|
||||
s.appendLeft(id.end! + offset, '.value')
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -331,7 +384,7 @@ export function transformAST(
|
||||
}
|
||||
|
||||
// check root scope first
|
||||
walkScope(ast)
|
||||
walkScope(ast, true)
|
||||
;(walk as any)(ast, {
|
||||
enter(node: Node, parent?: Node) {
|
||||
parent && parentStack.push(parent)
|
||||
@@ -371,7 +424,7 @@ export function transformAST(
|
||||
// walk up the scope chain to check if id should be appended .value
|
||||
let i = scopeStack.length
|
||||
while (i--) {
|
||||
if (checkRefId(scopeStack[i], node, parent!, parentStack)) {
|
||||
if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -424,7 +477,7 @@ export function transformAST(
|
||||
})
|
||||
|
||||
return {
|
||||
rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
|
||||
rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
|
||||
importedHelpers: [...importedHelpers]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user