feat(compiler): handle complex destructure expressions in v-for

This commit is contained in:
Evan You 2019-09-28 16:02:08 -04:00
parent 798a9cbe9b
commit 389a07835c
9 changed files with 388 additions and 278 deletions

View File

@ -4,13 +4,11 @@ import {
ElementNode, ElementNode,
DirectiveNode, DirectiveNode,
NodeTypes, NodeTypes,
ForNode,
CompilerOptions, CompilerOptions,
IfNode, IfNode,
InterpolationNode InterpolationNode
} from '../../src' } from '../../src'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithExpressionTransform( function parseWithExpressionTransform(
@ -20,7 +18,7 @@ function parseWithExpressionTransform(
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
prefixIdentifiers: true, prefixIdentifiers: true,
nodeTransforms: [transformIf, transformFor, transformExpression], nodeTransforms: [transformIf, transformExpression],
...options ...options
}) })
return ast.children[0] return ast.children[0]
@ -169,103 +167,6 @@ describe('compiler: expression transform', () => {
}) })
}) })
test('should prefix v-for source', () => {
const node = parseWithExpressionTransform(
`<div v-for="i in list"/>`
) as ForNode
expect(node.source).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.list`
})
})
test('should prefix v-for source w/ complex expression', () => {
const node = parseWithExpressionTransform(
`<div v-for="i in list.concat([foo])"/>`
) as ForNode
expect(node.source).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.list` },
`.`,
{ content: `concat` },
`([`,
{ content: `_ctx.foo` },
`])`
]
})
})
test('should not prefix v-for alias', () => {
const node = parseWithExpressionTransform(
`<div v-for="i in list">{{ i }}{{ j }}</div>`
) as ForNode
const div = node.children[0] as ElementNode
expect((div.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`
})
expect((div.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.j`
})
})
test('should not prefix v-for aliases (multiple)', () => {
const node = parseWithExpressionTransform(
`<div v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</div>`
) as ForNode
const div = node.children[0] as ElementNode
expect((div.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `i` },
` + `,
{ content: `j` },
` + `,
{ content: `k` }
]
})
expect((div.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.l`
})
})
test('should prefix id outside of v-for', () => {
const node = parseWithExpressionTransform(
`<div><div v-for="i in list" />{{ i }}</div>`
) as ElementNode
expect((node.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.i`
})
})
test('nested v-for', () => {
const node = parseWithExpressionTransform(
`<div v-for="i in list">
<div v-for="i in list">{{ i + j }}</div>{{ i }}
</div>`
) as ForNode
const outerDiv = node.children[0] as ElementNode
const innerFor = outerDiv.children[0] as ForNode
const innerExp = (innerFor.children[0] as ElementNode)
.children[0] as InterpolationNode
expect(innerExp.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: 'i' }, ` + `, { content: `_ctx.j` }]
})
// when an inner v-for shadows a variable of an outer v-for and exit,
// it should not cause the outer v-for's alias to be removed from known ids
const outerExp = outerDiv.children[1] as InterpolationNode
expect(outerExp.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`
})
})
test('should not prefix whitelisted globals', () => { test('should not prefix whitelisted globals', () => {
const node = parseWithExpressionTransform( const node = parseWithExpressionTransform(
`{{ Math.max(1, 2) }}` `{{ Math.max(1, 2) }}`
@ -334,7 +235,9 @@ describe('compiler: expression transform', () => {
expect(node.content).toMatchObject({ expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
`({ foo }) => `, `({ `,
{ content: `foo` },
` }) => `,
{ content: `foo` }, { content: `foo` },
` + `, ` + `,
{ content: `_ctx.bar` } { content: `_ctx.bar` }

View File

@ -1,148 +1,185 @@
import { parse } from '../../src/parse' import { parse } from '../../src/parse'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'
import { ForNode, NodeTypes, SimpleExpressionNode } from '../../src/ast' import {
ForNode,
NodeTypes,
SimpleExpressionNode,
ElementNode,
InterpolationNode
} from '../../src/ast'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { CompilerOptions } from '../../src' import { CompilerOptions } from '../../src'
import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithForTransform( function parseWithForTransform(
template: string, template: string,
options: CompilerOptions = {} options: CompilerOptions = {}
): ForNode { ) {
const node = parse(template, options) const node = parse(template, options)
transform(node, { nodeTransforms: [transformFor], ...options }) transform(node, {
if (!options.onError) { nodeTransforms: [
expect(node.children.length).toBe(1) transformFor,
expect(node.children[0].type).toBe(NodeTypes.FOR) ...(options.prefixIdentifiers ? [transformExpression] : [])
} ],
return node.children[0] as ForNode ...options
})
return node.children[0]
} }
describe('compiler: transform v-for', () => { describe('compiler: transform v-for', () => {
test('number expression', () => { test('number expression', () => {
const forNode = parseWithForTransform('<span v-for="index in 5" />') const forNode = parseWithForTransform(
'<span v-for="index in 5" />'
) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('index') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('index')
expect((forNode.source as SimpleExpressionNode).content).toBe('5') expect((forNode.source as SimpleExpressionNode).content).toBe('5')
}) })
test('value', () => { test('value', () => {
const forNode = parseWithForTransform('<span v-for="(item) in items" />') const forNode = parseWithForTransform(
'<span v-for="(item) in items" />'
) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('item') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('object de-structured value', () => { test('object de-structured value', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="({ id, value }) in items" />' '<span v-for="({ id, value }) in items" />'
) ) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('{ id, value }') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
'{ id, value }'
)
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('array de-structured value', () => { test('array de-structured value', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="([ id, value ]) in items" />' '<span v-for="([ id, value ]) in items" />'
) ) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('[ id, value ]') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
'[ id, value ]'
)
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('value and key', () => { test('value and key', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="(item, key) in items" />' '<span v-for="(item, key) in items" />'
) ) as ForNode
expect(forNode.keyAlias).not.toBeUndefined() expect(forNode.keyAlias).not.toBeUndefined()
expect(forNode.keyAlias!.content).toBe('key') expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('item') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('value, key and index', () => { test('value, key and index', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="(value, key, index) in items" />' '<span v-for="(value, key, index) in items" />'
) ) as ForNode
expect(forNode.keyAlias).not.toBeUndefined() expect(forNode.keyAlias).not.toBeUndefined()
expect(forNode.keyAlias!.content).toBe('key') expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
expect(forNode.valueAlias!.content).toBe('value') 'index'
)
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('skipped key', () => { test('skipped key', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="(value,,index) in items" />' '<span v-for="(value,,index) in items" />'
) ) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
expect(forNode.valueAlias!.content).toBe('value') 'index'
)
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('skipped value and key', () => { test('skipped value and key', () => {
const forNode = parseWithForTransform('<span v-for="(,,index) in items" />') const forNode = parseWithForTransform(
'<span v-for="(,,index) in items" />'
) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
'index'
)
expect(forNode.valueAlias).toBeUndefined() expect(forNode.valueAlias).toBeUndefined()
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('unbracketed value', () => { test('unbracketed value', () => {
const forNode = parseWithForTransform('<span v-for="item in items" />') const forNode = parseWithForTransform(
'<span v-for="item in items" />'
) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('item') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('unbracketed value and key', () => { test('unbracketed value and key', () => {
const forNode = parseWithForTransform('<span v-for="item, key in items" />') const forNode = parseWithForTransform(
'<span v-for="item, key in items" />'
) as ForNode
expect(forNode.keyAlias).not.toBeUndefined() expect(forNode.keyAlias).not.toBeUndefined()
expect(forNode.keyAlias!.content).toBe('key') expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
expect(forNode.objectIndexAlias).toBeUndefined() expect(forNode.objectIndexAlias).toBeUndefined()
expect(forNode.valueAlias!.content).toBe('item') expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('unbracketed value, key and index', () => { test('unbracketed value, key and index', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="value, key, index in items" />' '<span v-for="value, key, index in items" />'
) ) as ForNode
expect(forNode.keyAlias).not.toBeUndefined() expect(forNode.keyAlias).not.toBeUndefined()
expect(forNode.keyAlias!.content).toBe('key') expect((forNode.keyAlias as SimpleExpressionNode).content).toBe('key')
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
expect(forNode.valueAlias!.content).toBe('value') 'index'
)
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('unbracketed skipped key', () => { test('unbracketed skipped key', () => {
const forNode = parseWithForTransform( const forNode = parseWithForTransform(
'<span v-for="value, , index in items" />' '<span v-for="value, , index in items" />'
) ) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
expect(forNode.valueAlias!.content).toBe('value') 'index'
)
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('value')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
test('unbracketed skipped value and key', () => { test('unbracketed skipped value and key', () => {
const forNode = parseWithForTransform('<span v-for=", , index in items" />') const forNode = parseWithForTransform(
'<span v-for=", , index in items" />'
) as ForNode
expect(forNode.keyAlias).toBeUndefined() expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).not.toBeUndefined() expect(forNode.objectIndexAlias).not.toBeUndefined()
expect(forNode.objectIndexAlias!.content).toBe('index') expect((forNode.objectIndexAlias as SimpleExpressionNode).content).toBe(
'index'
)
expect(forNode.valueAlias).toBeUndefined() expect(forNode.valueAlias).toBeUndefined()
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
}) })
@ -210,17 +247,16 @@ describe('compiler: transform v-for', () => {
describe('source location', () => { describe('source location', () => {
test('value & source', () => { test('value & source', () => {
const source = '<span v-for="item in items" />' const source = '<span v-for="item in items" />'
const forNode = parseWithForTransform(source) const forNode = parseWithForTransform(source) as ForNode
const itemOffset = source.indexOf('item') const itemOffset = source.indexOf('item')
expect(forNode.valueAlias!.content).toBe('item') const value = forNode.valueAlias as SimpleExpressionNode
expect(forNode.valueAlias!.loc.start.offset).toBe(itemOffset) expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect(forNode.valueAlias!.loc.start.line).toBe(1) expect(value.loc.start.offset).toBe(itemOffset)
expect(forNode.valueAlias!.loc.start.column).toBe(itemOffset + 1) expect(value.loc.start.line).toBe(1)
expect(forNode.valueAlias!.loc.end.line).toBe(1) expect(value.loc.start.column).toBe(itemOffset + 1)
expect(forNode.valueAlias!.loc.end.column).toBe( expect(value.loc.end.line).toBe(1)
itemOffset + 1 + `item`.length expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
)
const itemsOffset = source.indexOf('items') const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
@ -235,17 +271,16 @@ describe('compiler: transform v-for', () => {
test('bracketed value', () => { test('bracketed value', () => {
const source = '<span v-for="( item ) in items" />' const source = '<span v-for="( item ) in items" />'
const forNode = parseWithForTransform(source) const forNode = parseWithForTransform(source) as ForNode
const itemOffset = source.indexOf('item') const itemOffset = source.indexOf('item')
expect(forNode.valueAlias!.content).toBe('item') const value = forNode.valueAlias as SimpleExpressionNode
expect(forNode.valueAlias!.loc.start.offset).toBe(itemOffset) expect(value.content).toBe('item')
expect(forNode.valueAlias!.loc.start.line).toBe(1) expect(value.loc.start.offset).toBe(itemOffset)
expect(forNode.valueAlias!.loc.start.column).toBe(itemOffset + 1) expect(value.loc.start.line).toBe(1)
expect(forNode.valueAlias!.loc.end.line).toBe(1) expect(value.loc.start.column).toBe(itemOffset + 1)
expect(forNode.valueAlias!.loc.end.column).toBe( expect(value.loc.end.line).toBe(1)
itemOffset + 1 + `item`.length expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
)
const itemsOffset = source.indexOf('items') const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
@ -260,17 +295,16 @@ describe('compiler: transform v-for', () => {
test('de-structured value', () => { test('de-structured value', () => {
const source = '<span v-for="( { id, key }) in items" />' const source = '<span v-for="( { id, key }) in items" />'
const forNode = parseWithForTransform(source) const forNode = parseWithForTransform(source) as ForNode
const value = forNode.valueAlias as SimpleExpressionNode
const valueIndex = source.indexOf('{ id, key }') const valueIndex = source.indexOf('{ id, key }')
expect(forNode.valueAlias!.content).toBe('{ id, key }') expect(value.content).toBe('{ id, key }')
expect(forNode.valueAlias!.loc.start.offset).toBe(valueIndex) expect(value.loc.start.offset).toBe(valueIndex)
expect(forNode.valueAlias!.loc.start.line).toBe(1) expect(value.loc.start.line).toBe(1)
expect(forNode.valueAlias!.loc.start.column).toBe(valueIndex + 1) expect(value.loc.start.column).toBe(valueIndex + 1)
expect(forNode.valueAlias!.loc.end.line).toBe(1) expect(value.loc.end.line).toBe(1)
expect(forNode.valueAlias!.loc.end.column).toBe( expect(value.loc.end.column).toBe(valueIndex + 1 + '{ id, key }'.length)
valueIndex + 1 + '{ id, key }'.length
)
const itemsOffset = source.indexOf('items') const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
@ -285,37 +319,34 @@ describe('compiler: transform v-for', () => {
test('bracketed value, key, index', () => { test('bracketed value, key, index', () => {
const source = '<span v-for="( item, key, index ) in items" />' const source = '<span v-for="( item, key, index ) in items" />'
const forNode = parseWithForTransform(source) const forNode = parseWithForTransform(source) as ForNode
const itemOffset = source.indexOf('item') const itemOffset = source.indexOf('item')
expect(forNode.valueAlias!.content).toBe('item') const value = forNode.valueAlias as SimpleExpressionNode
expect(forNode.valueAlias!.loc.start.offset).toBe(itemOffset) expect(value.content).toBe('item')
expect(forNode.valueAlias!.loc.start.line).toBe(1) expect(value.loc.start.offset).toBe(itemOffset)
expect(forNode.valueAlias!.loc.start.column).toBe(itemOffset + 1) expect(value.loc.start.line).toBe(1)
expect(forNode.valueAlias!.loc.end.line).toBe(1) expect(value.loc.start.column).toBe(itemOffset + 1)
expect(forNode.valueAlias!.loc.end.column).toBe( expect(value.loc.end.line).toBe(1)
itemOffset + 1 + `item`.length expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
)
const keyOffset = source.indexOf('key') const keyOffset = source.indexOf('key')
expect(forNode.keyAlias!.content).toBe('key') const key = forNode.keyAlias as SimpleExpressionNode
expect(forNode.keyAlias!.loc.start.offset).toBe(keyOffset) expect(key.content).toBe('key')
expect(forNode.keyAlias!.loc.start.line).toBe(1) expect(key.loc.start.offset).toBe(keyOffset)
expect(forNode.keyAlias!.loc.start.column).toBe(keyOffset + 1) expect(key.loc.start.line).toBe(1)
expect(forNode.keyAlias!.loc.end.line).toBe(1) expect(key.loc.start.column).toBe(keyOffset + 1)
expect(forNode.keyAlias!.loc.end.column).toBe( expect(key.loc.end.line).toBe(1)
keyOffset + 1 + `key`.length expect(key.loc.end.column).toBe(keyOffset + 1 + `key`.length)
)
const indexOffset = source.indexOf('index') const indexOffset = source.indexOf('index')
expect(forNode.objectIndexAlias!.content).toBe('index') const index = forNode.objectIndexAlias as SimpleExpressionNode
expect(forNode.objectIndexAlias!.loc.start.offset).toBe(indexOffset) expect(index.content).toBe('index')
expect(forNode.objectIndexAlias!.loc.start.line).toBe(1) expect(index.loc.start.offset).toBe(indexOffset)
expect(forNode.objectIndexAlias!.loc.start.column).toBe(indexOffset + 1) expect(index.loc.start.line).toBe(1)
expect(forNode.objectIndexAlias!.loc.end.line).toBe(1) expect(index.loc.start.column).toBe(indexOffset + 1)
expect(forNode.objectIndexAlias!.loc.end.column).toBe( expect(index.loc.end.line).toBe(1)
indexOffset + 1 + `index`.length expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
)
const itemsOffset = source.indexOf('items') const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
@ -330,27 +361,25 @@ describe('compiler: transform v-for', () => {
test('skipped key', () => { test('skipped key', () => {
const source = '<span v-for="( item,, index ) in items" />' const source = '<span v-for="( item,, index ) in items" />'
const forNode = parseWithForTransform(source) const forNode = parseWithForTransform(source) as ForNode
const itemOffset = source.indexOf('item') const itemOffset = source.indexOf('item')
expect(forNode.valueAlias!.content).toBe('item') const value = forNode.valueAlias as SimpleExpressionNode
expect(forNode.valueAlias!.loc.start.offset).toBe(itemOffset) expect(value.content).toBe('item')
expect(forNode.valueAlias!.loc.start.line).toBe(1) expect(value.loc.start.offset).toBe(itemOffset)
expect(forNode.valueAlias!.loc.start.column).toBe(itemOffset + 1) expect(value.loc.start.line).toBe(1)
expect(forNode.valueAlias!.loc.end.line).toBe(1) expect(value.loc.start.column).toBe(itemOffset + 1)
expect(forNode.valueAlias!.loc.end.column).toBe( expect(value.loc.end.line).toBe(1)
itemOffset + 1 + `item`.length expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
)
const indexOffset = source.indexOf('index') const indexOffset = source.indexOf('index')
expect(forNode.objectIndexAlias!.content).toBe('index') const index = forNode.objectIndexAlias as SimpleExpressionNode
expect(forNode.objectIndexAlias!.loc.start.offset).toBe(indexOffset) expect(index.content).toBe('index')
expect(forNode.objectIndexAlias!.loc.start.line).toBe(1) expect(index.loc.start.offset).toBe(indexOffset)
expect(forNode.objectIndexAlias!.loc.start.column).toBe(indexOffset + 1) expect(index.loc.start.line).toBe(1)
expect(forNode.objectIndexAlias!.loc.end.line).toBe(1) expect(index.loc.start.column).toBe(indexOffset + 1)
expect(forNode.objectIndexAlias!.loc.end.column).toBe( expect(index.loc.end.line).toBe(1)
indexOffset + 1 + `index`.length expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
)
const itemsOffset = source.indexOf('items') const itemsOffset = source.indexOf('items')
expect((forNode.source as SimpleExpressionNode).content).toBe('items') expect((forNode.source as SimpleExpressionNode).content).toBe('items')
@ -363,4 +392,146 @@ describe('compiler: transform v-for', () => {
) )
}) })
}) })
describe('prefixIdentifiers: true', () => {
test('should prefix v-for source', () => {
const node = parseWithForTransform(`<div v-for="i in list"/>`, {
prefixIdentifiers: true
}) as ForNode
expect(node.source).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.list`
})
})
test('should prefix v-for source w/ complex expression', () => {
const node = parseWithForTransform(
`<div v-for="i in list.concat([foo])"/>`,
{ prefixIdentifiers: true }
) as ForNode
expect(node.source).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.list` },
`.`,
{ content: `concat` },
`([`,
{ content: `_ctx.foo` },
`])`
]
})
})
test('should not prefix v-for alias', () => {
const node = parseWithForTransform(
`<div v-for="i in list">{{ i }}{{ j }}</div>`,
{ prefixIdentifiers: true }
) as ForNode
const div = node.children[0] as ElementNode
expect((div.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`
})
expect((div.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.j`
})
})
test('should not prefix v-for aliases (multiple)', () => {
const node = parseWithForTransform(
`<div v-for="(i, j, k) in list">{{ i + j + k }}{{ l }}</div>`,
{ prefixIdentifiers: true }
) as ForNode
const div = node.children[0] as ElementNode
expect((div.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `i` },
` + `,
{ content: `j` },
` + `,
{ content: `k` }
]
})
expect((div.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.l`
})
})
test('should prefix id outside of v-for', () => {
const node = parseWithForTransform(
`<div><div v-for="i in list" />{{ i }}</div>`,
{ prefixIdentifiers: true }
) as ElementNode
expect((node.children[1] as InterpolationNode).content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.i`
})
})
test('nested v-for', () => {
const node = parseWithForTransform(
`<div v-for="i in list">
<div v-for="i in list">{{ i + j }}</div>{{ i }}
</div>`,
{ prefixIdentifiers: true }
) as ForNode
const outerDiv = node.children[0] as ElementNode
const innerFor = outerDiv.children[0] as ForNode
const innerExp = (innerFor.children[0] as ElementNode)
.children[0] as InterpolationNode
expect(innerExp.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: 'i' }, ` + `, { content: `_ctx.j` }]
})
// when an inner v-for shadows a variable of an outer v-for and exit,
// it should not cause the outer v-for's alias to be removed from known ids
const outerExp = outerDiv.children[1] as InterpolationNode
expect(outerExp.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `i`
})
})
test('v-for aliases w/ complex expressions', () => {
const node = parseWithForTransform(
`<div v-for="({ foo = bar, baz: [qux = quux] }) in list">
{{ foo + bar + baz + qux + quux }}
</div>`,
{ prefixIdentifiers: true }
) as ForNode
expect(node.valueAlias!).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`{ `,
{ content: `foo` },
` = `,
{ content: `_ctx.bar` },
`, baz: [`,
{ content: `qux` },
` = `,
{ content: `_ctx.quux` },
`] }`
]
})
const div = node.children[0] as ElementNode
expect((div.children[0] as InterpolationNode).content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `foo` },
` + `,
{ content: `_ctx.bar` },
` + `,
{ content: `_ctx.baz` },
` + `,
{ content: `qux` },
` + `,
{ content: `_ctx.quux` }
]
})
})
})
}) })

View File

@ -83,9 +83,8 @@ describe('compiler: transform component slots', () => {
default: { default: {
type: NodeTypes.JS_SLOT_FUNCTION, type: NodeTypes.JS_SLOT_FUNCTION,
params: { params: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
content: `{ foo }`, children: [`{ `, { content: `foo` }, ` }`]
isStatic: false
}, },
returns: [ returns: [
{ {
@ -266,9 +265,8 @@ describe('compiler: transform component slots', () => {
default: { default: {
type: NodeTypes.JS_SLOT_FUNCTION, type: NodeTypes.JS_SLOT_FUNCTION,
params: { params: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
content: `{ bar }`, children: [`{ `, { content: `bar` }, ` }`]
isStatic: false
}, },
returns: [ returns: [
{ {

View File

@ -150,9 +150,9 @@ export interface IfBranchNode extends Node {
export interface ForNode extends Node { export interface ForNode extends Node {
type: NodeTypes.FOR type: NodeTypes.FOR
source: ExpressionNode source: ExpressionNode
valueAlias: SimpleExpressionNode | undefined valueAlias: ExpressionNode | undefined
keyAlias: SimpleExpressionNode | undefined keyAlias: ExpressionNode | undefined
objectIndexAlias: SimpleExpressionNode | undefined objectIndexAlias: ExpressionNode | undefined
children: ChildNode[] children: ChildNode[]
} }

View File

@ -509,14 +509,14 @@ function genFor(node: ForNode, context: CodegenContext) {
genNode(source, context) genNode(source, context)
push(`, (`) push(`, (`)
if (valueAlias) { if (valueAlias) {
genExpression(valueAlias, context) genNode(valueAlias, context)
} }
if (keyAlias) { if (keyAlias) {
if (!valueAlias) { if (!valueAlias) {
push(`__value`) push(`__value`)
} }
push(`, `) push(`, `)
genExpression(keyAlias, context) genNode(keyAlias, context)
} }
if (objectIndexAlias) { if (objectIndexAlias) {
if (!keyAlias) { if (!keyAlias) {
@ -527,7 +527,7 @@ function genFor(node: ForNode, context: CodegenContext) {
} }
} }
push(`, `) push(`, `)
genExpression(objectIndexAlias, context) genNode(objectIndexAlias, context)
} }
push(`) => {`) push(`) => {`)
indent() indent()

View File

@ -63,8 +63,8 @@ export interface TransformContext extends Required<TransformOptions> {
replaceNode(node: ChildNode): void replaceNode(node: ChildNode): void
removeNode(node?: ChildNode): void removeNode(node?: ChildNode): void
onNodeRemoved: () => void onNodeRemoved: () => void
addIdentifier(id: string): void addIdentifiers(exp: ExpressionNode): void
removeIdentifier(id: string): void removeIdentifiers(exp: ExpressionNode): void
hoist(exp: JSChildNode): ExpressionNode hoist(exp: JSChildNode): ExpressionNode
} }
@ -126,15 +126,24 @@ function createTransformContext(
context.parent.children.splice(removalIndex, 1) context.parent.children.splice(removalIndex, 1)
}, },
onNodeRemoved: () => {}, onNodeRemoved: () => {},
addIdentifier(id) { addIdentifiers(exp) {
const { identifiers } = context // identifier tracking only happens in non-browser builds.
if (identifiers[id] === undefined) { if (!__BROWSER__) {
identifiers[id] = 0 if (exp.identifiers) {
exp.identifiers.forEach(addId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
addId(exp.content)
}
} }
;(identifiers[id] as number)++
}, },
removeIdentifier(id) { removeIdentifiers(exp) {
;(context.identifiers[id] as number)-- if (!__BROWSER__) {
if (exp.identifiers) {
exp.identifiers.forEach(removeId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
removeId(exp.content)
}
}
}, },
hoist(exp) { hoist(exp) {
context.hoists.push(exp) context.hoists.push(exp)
@ -145,6 +154,19 @@ function createTransformContext(
) )
} }
} }
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
;(identifiers[id] as number)++
}
function removeId(id: string) {
;(context.identifiers[id] as number)--
}
return context return context
} }

View File

@ -67,6 +67,8 @@ interface PrefixMeta {
export function processExpression( export function processExpression(
node: SimpleExpressionNode, node: SimpleExpressionNode,
context: TransformContext, context: TransformContext,
// some expressions like v-slot props & v-for aliases should be parsed as
// function params
asParams: boolean = false asParams: boolean = false
): ExpressionNode { ): ExpressionNode {
if (!context.prefixIdentifiers) { if (!context.prefixIdentifiers) {
@ -75,7 +77,7 @@ export function processExpression(
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
if (isSimpleIdentifier(node.content)) { if (isSimpleIdentifier(node.content)) {
if (!context.identifiers[node.content]) { if (!asParams && !context.identifiers[node.content]) {
node.content = `_ctx.${node.content}` node.content = `_ctx.${node.content}`
} }
return node return node
@ -107,17 +109,14 @@ export function processExpression(
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
if (!ids.includes(node)) { if (!ids.includes(node)) {
if (!knownIds[node.name] && shouldPrefix(node, parent)) { if (!knownIds[node.name] && shouldPrefix(node, parent)) {
if ( if (isPropertyShorthand(node, parent)) {
isPropertyKey(node, parent) &&
(parent as Property).value === node
) {
// property shorthand like { foo }, we need to add the key since we // property shorthand like { foo }, we need to add the key since we
// rewrite the value // rewrite the value
node.prefix = `${node.name}: ` node.prefix = `${node.name}: `
} }
node.name = `_ctx.${node.name}` node.name = `_ctx.${node.name}`
ids.push(node) ids.push(node)
} else if (!isPropertyKey(node, parent)) { } else if (!isStaticPropertyKey(node, parent)) {
// also generate sub-expressioms for other identifiers for better // also generate sub-expressioms for other identifiers for better
// source map support. (except for property keys which are static) // source map support. (except for property keys which are static)
ids.push(node) ids.push(node)
@ -131,9 +130,11 @@ export function processExpression(
enter(child, parent) { enter(child, parent) {
if ( if (
child.type === 'Identifier' && child.type === 'Identifier' &&
!// do not keep as scope variable if this is a default value // do not record as scope variable if is a destrcuture key
// assignment of a param !isStaticPropertyKey(child, parent) &&
( // do not record if this is a default value
// assignment of a destructured variable
!(
parent && parent &&
parent.type === 'AssignmentPattern' && parent.type === 'AssignmentPattern' &&
parent.right === child parent.right === child
@ -213,7 +214,16 @@ const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type) /Function(Expression|Declaration)$/.test(node.type)
const isPropertyKey = (node: Node, parent: Node) => const isPropertyKey = (node: Node, parent: Node) =>
parent.type === 'Property' && parent.key === node && !parent.computed parent &&
parent.type === 'Property' &&
parent.key === node &&
!parent.computed
const isPropertyShorthand = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value === node
const isStaticPropertyKey = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value !== node
const globals = new Set( const globals = new Set(
( (
@ -236,11 +246,7 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
parent.params.includes(identifier)) parent.params.includes(identifier))
) && ) &&
// not a key of Property // not a key of Property
!( !isStaticPropertyKey(identifier, parent) &&
isPropertyKey(identifier, parent) &&
// shorthand keys should be prefixed
!((parent as Property).value === identifier)
) &&
// not a property of a MemberExpression // not a property of a MemberExpression
!( !(
parent.type === 'MemberExpression' && parent.type === 'MemberExpression' &&

View File

@ -39,19 +39,20 @@ export const transformFor = createStructuralDirectiveTransform(
children: [node] children: [node]
}) })
// scope management if (!__BROWSER__) {
const { addIdentifier, removeIdentifier } = context // scope management
const { addIdentifiers, removeIdentifiers } = context
// inject identifiers to context // inject identifiers to context
value && addIdentifier(value.content) value && addIdentifiers(value)
key && addIdentifier(key.content) key && addIdentifiers(key)
index && addIdentifier(index.content) index && addIdentifiers(index)
return () => { return () => {
// remove injected identifiers on exit value && removeIdentifiers(value)
value && removeIdentifier(value.content) key && removeIdentifiers(key)
key && removeIdentifier(key.content) index && removeIdentifiers(index)
index && removeIdentifier(index.content) }
} }
} else { } else {
context.onError( context.onError(
@ -67,14 +68,16 @@ export const transformFor = createStructuralDirectiveTransform(
) )
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g const stripParensRE = /^\(|\)$/g
interface ForParseResult { interface ForParseResult {
source: ExpressionNode source: ExpressionNode
value: SimpleExpressionNode | undefined value: ExpressionNode | undefined
key: SimpleExpressionNode | undefined key: ExpressionNode | undefined
index: SimpleExpressionNode | undefined index: ExpressionNode | undefined
} }
function parseForExpression( function parseForExpression(
@ -88,21 +91,22 @@ function parseForExpression(
const [, LHS, RHS] = inMatch const [, LHS, RHS] = inMatch
let source: ExpressionNode = createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
)
if (!__BROWSER__ && context.prefixIdentifiers) {
source = processExpression(source, context)
}
const result: ForParseResult = { const result: ForParseResult = {
source, source: createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
),
value: undefined, value: undefined,
key: undefined, key: undefined,
index: undefined index: undefined
} }
if (!__BROWSER__ && context.prefixIdentifiers) {
result.source = processExpression(
result.source as SimpleExpressionNode,
context
)
}
let valueContent = LHS.trim() let valueContent = LHS.trim()
.replace(stripParensRE, '') .replace(stripParensRE, '')
@ -118,6 +122,9 @@ function parseForExpression(
if (keyContent) { if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length) keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(loc, keyContent, keyOffset) result.key = createAliasExpression(loc, keyContent, keyOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.key = processExpression(result.key, context, true)
}
} }
if (iteratorMatch[2]) { if (iteratorMatch[2]) {
@ -134,12 +141,18 @@ function parseForExpression(
: trimmedOffset + valueContent.length : trimmedOffset + valueContent.length
) )
) )
if (!__BROWSER__ && context.prefixIdentifiers) {
result.index = processExpression(result.index, context, true)
}
} }
} }
} }
if (valueContent) { if (valueContent) {
result.value = createAliasExpression(loc, valueContent, trimmedOffset) result.value = createAliasExpression(loc, valueContent, trimmedOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.value = processExpression(result.value, context, true)
}
} }
return result return result

View File

@ -31,12 +31,9 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
) { ) {
const vSlot = node.props.find(isVSlot) const vSlot = node.props.find(isVSlot)
if (vSlot && vSlot.exp) { if (vSlot && vSlot.exp) {
const { identifiers } = vSlot.exp context.addIdentifiers(vSlot.exp)
if (identifiers) { return () => {
identifiers.forEach(context.addIdentifier) context.removeIdentifiers(vSlot.exp!)
return () => {
identifiers.forEach(context.removeIdentifier)
}
} }
} }
} }