feat(experimental): support ref transform for sfc normal <script>

This commit is contained in:
Evan You 2021-08-23 16:00:46 -04:00
parent f173cf0026
commit 06051c4bf2
8 changed files with 226 additions and 74 deletions

View File

@ -4,7 +4,8 @@ import {
Node, Node,
Function, Function,
ObjectProperty, ObjectProperty,
BlockStatement BlockStatement,
Program
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
@ -149,16 +150,23 @@ export function walkFunctionParams(
} }
export function walkBlockDeclarations( export function walkBlockDeclarations(
block: BlockStatement, block: BlockStatement | Program,
onIdent: (node: Identifier) => void onIdent: (node: Identifier) => void
) { ) {
for (const stmt of block.body) { for (const stmt of block.body) {
if (stmt.type === 'VariableDeclaration') { if (stmt.type === 'VariableDeclaration') {
if (stmt.declare) continue
for (const decl of stmt.declarations) { for (const decl of stmt.declarations) {
for (const id of extractIdentifiers(decl.id)) { for (const id of extractIdentifiers(decl.id)) {
onIdent(id) onIdent(id)
} }
} }
} else if (
stmt.type === 'FunctionDeclaration' ||
stmt.type === 'ClassDeclaration'
) {
if (stmt.declare || !stmt.id) continue
onIdent(stmt.id)
} }
} }
} }

View File

@ -1,43 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<script setup> ref sugar $ unwrapping 1`] = `
"import { ref, shallowRef } from 'vue'
export default {
setup(__props, { expose }) {
expose()
let foo = (ref())
let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
return { foo, a, b, c, d, ref, shallowRef }
}
}"
`;
exports[`<script setup> ref sugar $ref & $shallowRef declarations 1`] = `
"import { ref as _ref, shallowRef as _shallowRef } from 'vue'
export default {
setup(__props, { expose }) {
expose()
let foo = _ref()
let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
return { foo, a, b, c, d }
}
}"
`;

View File

@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sfc ref transform $ unwrapping 1`] = `
"import { ref, shallowRef } from 'vue'
export default {
setup(__props, { expose }) {
expose()
let foo = (ref())
let a = (ref(1))
let b = (shallowRef({
count: 0
}))
let c = () => {}
let d
return { foo, a, b, c, d, 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()
let foo = _ref()
let a = _ref(1)
let b = _shallowRef({
count: 0
})
let c = () => {}
let d
return { foo, a, b, c, d }
}
}"
`;
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> + <script setup> 1`] = `
"import { ref as _ref } from 'vue'
let a = _ref(0)
let c = _ref(0)
export default {
setup(__props, { expose }) {
expose()
let b = _ref(0)
let c = 0
function change() {
a.value++
b.value++
c++
}
return { a, c, b, change }
}
}"
`;

View File

@ -3,13 +3,13 @@ import { compileSFCScript as compile, assertCode } from './utils'
// this file only tests integration with SFC - main test case for the ref // this file only tests integration with SFC - main test case for the ref
// transform can be found in <root>/packages/ref-transform/__tests__ // transform can be found in <root>/packages/ref-transform/__tests__
describe('<script setup> ref sugar', () => { describe('sfc ref transform', () => {
function compileWithRefSugar(src: string) { function compileWithRefTransform(src: string) {
return compile(src, { refSugar: true }) return compile(src, { refSugar: true })
} }
test('$ unwrapping', () => { test('$ unwrapping', () => {
const { content, bindings } = compileWithRefSugar(`<script setup> const { content, bindings } = compileWithRefTransform(`<script setup>
import { ref, shallowRef } from 'vue' import { ref, shallowRef } from 'vue'
let foo = $(ref()) let foo = $(ref())
let a = $(ref(1)) let a = $(ref(1))
@ -46,7 +46,7 @@ describe('<script setup> ref sugar', () => {
}) })
test('$ref & $shallowRef declarations', () => { test('$ref & $shallowRef declarations', () => {
const { content, bindings } = compileWithRefSugar(`<script setup> const { content, bindings } = compileWithRefTransform(`<script setup>
let foo = $ref() let foo = $ref()
let a = $ref(1) let a = $ref(1)
let b = $shallowRef({ let b = $shallowRef({
@ -81,6 +81,58 @@ describe('<script setup> ref sugar', () => {
}) })
}) })
test('usage in normal <script>', () => {
const { content } = compileWithRefTransform(`<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 with normal <script> + <script setup>', () => {
const { content, bindings } = compileWithRefTransform(`<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
})
})
describe('errors', () => { describe('errors', () => {
test('defineProps/Emit() referencing ref declarations', () => { test('defineProps/Emit() referencing ref declarations', () => {
expect(() => expect(() =>

View File

@ -165,12 +165,34 @@ export function compileScript(
return script return script
} }
try { try {
const scriptAst = _parse(script.content, { let content = script.content
let map = script.map
const scriptAst = _parse(content, {
plugins, plugins,
sourceType: 'module' sourceType: 'module'
}).program.body }).program
const bindings = analyzeScriptBindings(scriptAst) const bindings = analyzeScriptBindings(scriptAst.body)
let content = script.content if (enableRefTransform && shouldTransformRef(content)) {
const s = new MagicString(source)
const startOffset = script.loc.start.offset
const endOffset = script.loc.end.offset
const { importedHelpers } = transformRefAST(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()
map = s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap
}
if (cssVars.length) { if (cssVars.length) {
content = rewriteDefault(content, `__default__`, plugins) content = rewriteDefault(content, `__default__`, plugins)
content += genNormalScriptCssVarsCode( content += genNormalScriptCssVarsCode(
@ -184,8 +206,9 @@ export function compileScript(
return { return {
...script, ...script,
content, content,
map,
bindings, bindings,
scriptAst scriptAst: scriptAst.body
} }
} catch (e) { } catch (e) {
// silently fallback if parse fails since user may be using custom // silently fallback if parse fails since user may be using custom
@ -629,6 +652,23 @@ export function compileScript(
walkDeclaration(node, setupBindings, userImportAlias) walkDeclaration(node, setupBindings, userImportAlias)
} }
} }
// apply ref transform
if (enableRefTransform && shouldTransformRef(script.content)) {
warnExperimental(
`ref sugar`,
`https://github.com/vuejs/rfcs/discussions/369`
)
const { rootVars, importedHelpers } = transformRefAST(
scriptAst,
s,
scriptStartOffset!
)
refBindings = rootVars
for (const h of importedHelpers) {
helperImports.add(h)
}
}
} }
// 2. parse <script setup> and walk over top level statements // 2. parse <script setup> and walk over top level statements
@ -862,7 +902,7 @@ export function compileScript(
} }
// 3. Apply ref sugar transform // 3. Apply ref sugar transform
if (enableRefTransform && shouldTransformRef(source)) { if (enableRefTransform && shouldTransformRef(scriptSetup.content)) {
warnExperimental( warnExperimental(
`ref sugar`, `ref sugar`,
`https://github.com/vuejs/rfcs/discussions/369` `https://github.com/vuejs/rfcs/discussions/369`
@ -870,9 +910,10 @@ export function compileScript(
const { rootVars, importedHelpers } = transformRefAST( const { rootVars, importedHelpers } = transformRefAST(
scriptSetupAst, scriptSetupAst,
s, s,
startOffset startOffset,
refBindings
) )
refBindings = rootVars refBindings = refBindings ? [...refBindings, ...rootVars] : rootVars
for (const h of importedHelpers) { for (const h of importedHelpers) {
helperImports.add(h) helperImports.add(h)
} }

View File

@ -136,6 +136,9 @@ exports[`nested scopes 1`] = `
b.value++ // outer b b.value++ // outer b
c++ // outer c c++ // outer c
let bar = _ref(0)
bar.value++ // outer bar
function foo({ a }) { function foo({ a }) {
a++ // inner a a++ // inner a
b.value++ // inner b b.value++ // inner b
@ -143,10 +146,11 @@ exports[`nested scopes 1`] = `
c.value++ // inner c c.value++ // inner c
let d = _ref(0) let d = _ref(0)
const bar = (c) => { function bar(c) {
c++ // nested c c++ // nested c
d.value++ // nested d d.value++ // nested d
} }
bar() // inner bar
if (true) { if (true) {
let a = _ref(0) let a = _ref(0)

View File

@ -279,6 +279,9 @@ test('nested scopes', () => {
b++ // outer b b++ // outer b
c++ // outer c c++ // outer c
let bar = $ref(0)
bar++ // outer bar
function foo({ a }) { function foo({ a }) {
a++ // inner a a++ // inner a
b++ // inner b b++ // inner b
@ -286,10 +289,11 @@ test('nested scopes', () => {
c++ // inner c c++ // inner c
let d = $ref(0) let d = $ref(0)
const bar = (c) => { function bar(c) {
c++ // nested c c++ // nested c
d++ // nested d d++ // nested d
} }
bar() // inner bar
if (true) { if (true) {
let a = $ref(0) let a = $ref(0)
@ -299,7 +303,7 @@ test('nested scopes', () => {
return $$({ a, b, c, d }) return $$({ a, b, c, d })
} }
`) `)
expect(rootVars).toStrictEqual(['a', 'b']) expect(rootVars).toStrictEqual(['a', 'b', 'bar'])
expect(code).toMatch('a.value++ // outer a') expect(code).toMatch('a.value++ // outer a')
expect(code).toMatch('b.value++ // outer b') expect(code).toMatch('b.value++ // outer b')
@ -314,6 +318,10 @@ test('nested scopes', () => {
expect(code).toMatch(`a.value++ // if block a`) // if block 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 })`) expect(code).toMatch(`return ({ a, b, c, d })`)
assertCode(code) assertCode(code)
}) })

View File

@ -1,7 +1,6 @@
import { import {
Node, Node,
Identifier, Identifier,
VariableDeclarator,
BlockStatement, BlockStatement,
CallExpression, CallExpression,
ObjectPattern, ObjectPattern,
@ -30,14 +29,6 @@ export function shouldTransform(src: string): boolean {
return transformCheckRE.test(src) return transformCheckRE.test(src)
} }
export interface ReactiveDeclarator {
node: VariableDeclarator
statement: VariableDeclaration
ids: Identifier[]
isPattern: boolean
isRoot: boolean
}
type Scope = Record<string, boolean> type Scope = Record<string, boolean>
export interface RefTransformOptions { export interface RefTransformOptions {
@ -105,18 +96,26 @@ export function transform(
export function transformAST( export function transformAST(
ast: Node, ast: Node,
s: MagicString, s: MagicString,
offset = 0 offset = 0,
knownRootVars?: string[]
): { ): {
rootVars: string[] rootVars: string[]
importedHelpers: string[] importedHelpers: string[]
} { } {
const importedHelpers = new Set<string>() const importedHelpers = new Set<string>()
const blockStack: BlockStatement[] = [] const blockStack: BlockStatement[] = []
let currentBlock: BlockStatement | null = null
const rootScope: Scope = {} const rootScope: Scope = {}
const blockToScopeMap = new WeakMap<BlockStatement, Scope>() const blockToScopeMap = new WeakMap<BlockStatement, Scope>()
const excludedIds = new Set<Identifier>() const excludedIds = new Set<Identifier>()
const parentStack: Node[] = [] const parentStack: Node[] = []
if (knownRootVars) {
for (const key of knownRootVars) {
rootScope[key] = true
}
}
const error = (msg: string, node: Node) => { const error = (msg: string, node: Node) => {
const e = new Error(msg) const e = new Error(msg)
;(e as any).node = node ;(e as any).node = node
@ -130,7 +129,6 @@ export function transformAST(
const registerBinding = (id: Identifier, isRef = false) => { const registerBinding = (id: Identifier, isRef = false) => {
excludedIds.add(id) excludedIds.add(id)
const currentBlock = blockStack[blockStack.length - 1]
if (currentBlock) { if (currentBlock) {
const currentScope = blockToScopeMap.get(currentBlock) const currentScope = blockToScopeMap.get(currentBlock)
if (!currentScope) { if (!currentScope) {
@ -145,13 +143,16 @@ export function transformAST(
const registerRefBinding = (id: Identifier) => registerBinding(id, true) const registerRefBinding = (id: Identifier) => registerBinding(id, true)
if (ast.type === 'Program') {
walkBlockDeclarations(ast, registerBinding)
}
// 1st pass: detect macro callsites and register ref bindings // 1st pass: detect macro callsites and register ref bindings
;(walk as any)(ast, { ;(walk as any)(ast, {
enter(node: Node, parent?: Node) { enter(node: Node, parent?: Node) {
parent && parentStack.push(parent) parent && parentStack.push(parent)
if (node.type === 'BlockStatement') { if (node.type === 'BlockStatement') {
blockStack.push(node) blockStack.push((currentBlock = node))
walkBlockDeclarations(node, registerBinding) walkBlockDeclarations(node, registerBinding)
if (parent && isFunctionType(parent)) { if (parent && isFunctionType(parent)) {
walkFunctionParams(parent, registerBinding) walkFunctionParams(parent, registerBinding)
@ -213,6 +214,7 @@ export function transformAST(
parent && parentStack.pop() parent && parentStack.pop()
if (node.type === 'BlockStatement') { if (node.type === 'BlockStatement') {
blockStack.pop() blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
} }
} }
}) })
@ -356,7 +358,7 @@ export function transformAST(
} }
return { return {
rootVars: Object.keys(rootScope), rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
importedHelpers: [...importedHelpers] importedHelpers: [...importedHelpers]
} }
} }