transform,
ExpressionNode,
ElementNode,
- DirectiveNode
+ DirectiveNode,
+ NodeTypes,
+ ForNode
} from '../../src'
import { transformFor } from '../..//src/transforms/vFor'
import { transformExpression } from '../../src/transforms/transformExpression'
describe('compiler: expression transform', () => {
test('interpolation (root)', () => {
const node = parseWithExpressionTransform(`{{ foo }}`) as ExpressionNode
- expect(node.content).toBe(`_ctx.foo`)
+ expect(node.children).toMatchObject([
+ `_ctx.`,
+ {
+ content: `foo`,
+ loc: node.loc
+ }
+ ])
})
test('interpolation (children)', () => {
const node = parseWithExpressionTransform(
`<div>{{ foo }}</div>`
) as ElementNode
- expect((node.children[0] as ExpressionNode).content).toBe(`_ctx.foo`)
+ expect((node.children[0] as ExpressionNode).children).toMatchObject([
+ `_ctx.`,
+ {
+ content: `foo`,
+ loc: node.children[0].loc
+ }
+ ])
})
test('directive value', () => {
const node = parseWithExpressionTransform(
`<div v-foo:arg="baz"/>`
) as ElementNode
- expect((node.props[0] as DirectiveNode).arg!.content).toBe(`arg`)
- expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`)
+ expect((node.props[0] as DirectiveNode).arg!.children).toBeUndefined()
+ const exp = (node.props[0] as DirectiveNode).exp!
+ expect(exp.children).toMatchObject([
+ `_ctx.`,
+ {
+ content: `baz`,
+ loc: exp.loc
+ }
+ ])
})
test('dynamic directive arg', () => {
const node = parseWithExpressionTransform(
`<div v-foo:[arg]="baz"/>`
) as ElementNode
- expect((node.props[0] as DirectiveNode).arg!.content).toBe(`_ctx.arg`)
- expect((node.props[0] as DirectiveNode).exp!.content).toBe(`_ctx.baz`)
+ const arg = (node.props[0] as DirectiveNode).arg!
+ const exp = (node.props[0] as DirectiveNode).exp!
+ expect(arg.children).toMatchObject([
+ `_ctx.`,
+ {
+ content: `arg`,
+ loc: arg.loc
+ }
+ ])
+ expect(exp.children).toMatchObject([
+ `_ctx.`,
+ {
+ content: `baz`,
+ loc: exp.loc
+ }
+ ])
})
test('should prefix complex expressions', () => {
) as ExpressionNode
// should parse into compound expression
expect(node.children).toMatchObject([
- { content: `_ctx.foo` },
- `(`,
- { content: `_ctx.baz` },
- ` + 1, { key: `,
- { content: `_ctx.kuz` },
+ `_ctx.`,
+ {
+ content: `foo`,
+ loc: {
+ source: `foo`,
+ start: {
+ offset: 3,
+ line: 1,
+ column: 4
+ },
+ end: {
+ offset: 6,
+ line: 1,
+ column: 7
+ }
+ }
+ },
+ `(_ctx.`,
+ {
+ content: `baz`,
+ loc: {
+ source: `baz`,
+ start: {
+ offset: 7,
+ line: 1,
+ column: 8
+ },
+ end: {
+ offset: 10,
+ line: 1,
+ column: 11
+ }
+ }
+ },
+ ` + 1, { key: _ctx.`,
+ {
+ content: `kuz`,
+ loc: {
+ source: `kuz`,
+ start: {
+ offset: 23,
+ line: 1,
+ column: 24
+ },
+ end: {
+ offset: 26,
+ line: 1,
+ column: 27
+ }
+ }
+ },
` })`
])
})
- // TODO FIXME
- test('should not prefix v-for aliases', () => {
- // const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode
- // expect(node.children).toMatchObject([
- // `{ foo: `,
- // { 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
+
+ const i = div.children[0] as ExpressionNode
+ expect(i.type).toBe(NodeTypes.EXPRESSION)
+ expect(i.content).toBe(`i`)
+ expect(i.children).toBeUndefined()
+
+ const j = div.children[1] as ExpressionNode
+ expect(j.type).toBe(NodeTypes.EXPRESSION)
+ expect(j.children).toMatchObject([`_ctx.`, { content: `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
+
+ const exp = div.children[0] as ExpressionNode
+ expect(exp.type).toBe(NodeTypes.EXPRESSION)
+ expect(exp.content).toBe(`i + j + k`)
+ expect(exp.children).toBeUndefined()
+
+ const l = div.children[1] as ExpressionNode
+ expect(l.type).toBe(NodeTypes.EXPRESSION)
+ expect(l.children).toMatchObject([`_ctx.`, { content: `l` }])
+ })
+
+ test('should prefix id outside of v-for', () => {
+ const node = parseWithExpressionTransform(
+ `<div><div v-for="i in list" />{{ i }}</div>`
+ ) as ElementNode
+ const exp = node.children[1] as ExpressionNode
+ expect(exp.type).toBe(NodeTypes.EXPRESSION)
+ expect(exp.content).toBe(`i`)
+ expect(exp.children).toMatchObject([`_ctx.`, { content: `i` }])
})
- test('should prefix id outside of v-for', () => {})
+ 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 ExpressionNode
+ expect(innerExp.type).toBe(NodeTypes.EXPRESSION)
+ expect(innerExp.children).toMatchObject([`i + _ctx.`, { content: `j` }])
- test('nested v-for', () => {})
+ // 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 ExpressionNode
+ expect(outerExp.type).toBe(NodeTypes.EXPRESSION)
+ expect(outerExp.content).toBe(`i`)
+ expect(outerExp.children).toBeUndefined()
+ })
- test('should not prefix whitelisted globals', () => {})
+ test('should not prefix whitelisted globals', () => {
+ const node = parseWithExpressionTransform(
+ `{{ Math.max(1, 2) }}`
+ ) as ExpressionNode
+ expect(node.type).toBe(NodeTypes.EXPRESSION)
+ expect(node.content).toBe(`Math.max(1, 2)`)
+ expect(node.children).toBeUndefined()
+ })
- test('should not prefix id of a function declaration', () => {})
+ test('should not prefix id of a function declaration', () => {
+ const node = parseWithExpressionTransform(
+ `{{ function foo() { return bar } }}`
+ ) as ExpressionNode
+ expect(node.type).toBe(NodeTypes.EXPRESSION)
+ expect(node.children).toMatchObject([
+ `function foo() { return _ctx.`,
+ { content: `bar` },
+ ` }`
+ ])
+ })
test('should not prefix params of a function expression', () => {
- // also test object + array destructure
+ const node = parseWithExpressionTransform(
+ `{{ foo => foo + bar }}`
+ ) as ExpressionNode
+ expect(node.type).toBe(NodeTypes.EXPRESSION)
+ expect(node.children).toMatchObject([
+ `foo => foo + _ctx.`,
+ { content: `bar` }
+ ])
+ })
+
+ test('should not prefix an object property key', () => {
+ const node = parseWithExpressionTransform(
+ `{{ { foo: bar } }}`
+ ) as ExpressionNode
+ expect(node.type).toBe(NodeTypes.EXPRESSION)
+ expect(node.children).toMatchObject([
+ `{ foo: _ctx.`,
+ { content: `bar` },
+ ` }`
+ ])
})
- test('should not prefix an object property key', () => {})
+ test('should prefix a computed object property key', () => {
+ const node = parseWithExpressionTransform(
+ `{{ { [foo]: bar } }}`
+ ) as ExpressionNode
+ expect(node.type).toBe(NodeTypes.EXPRESSION)
+ expect(node.children).toMatchObject([
+ `{ [_ctx.`,
+ { content: `foo` },
+ `]: _ctx.`,
+ { content: `bar` },
+ ` }`
+ ])
+ })
- test('should prefix a computed object property key', () => {})
+ test('should prefix object property shorthand value', () => {
+ const node = parseWithExpressionTransform(`{{ { foo } }}`) as ExpressionNode
+ expect(node.children).toMatchObject([
+ `{ foo: _ctx.`,
+ { content: `foo` },
+ ` }`
+ ])
+ })
- test('should prefix object property shorthand value', () => {})
+ test('should not prefix id in a member expression', () => {
+ const node = parseWithExpressionTransform(
+ `{{ foo.bar.baz }}`
+ ) as ExpressionNode
+ expect(node.children).toMatchObject([
+ `_ctx.`,
+ { content: `foo` },
+ `.bar.baz`
+ ])
+ })
- test('should not prefix id in a member expression', () => {})
+ test('should prefix computed id in a member expression', () => {
+ const node = parseWithExpressionTransform(
+ `{{ foo[bar][baz] }}`
+ ) as ExpressionNode
+ expect(node.children).toMatchObject([
+ `_ctx.`,
+ { content: `foo` },
+ `[_ctx.`,
+ { content: `bar` },
+ `][_ctx.`,
+ { content: 'baz' },
+ `]`
+ ])
+ })
})
const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
+const isFunction = (node: Node): node is Function =>
+ /Function(Expression|Declaration)$/.test(node.type)
+
// cache node requires
let _parseScript: typeof parseScript
let _walk: typeof walk
+interface PrefixMeta {
+ prefix: string
+ start: number
+ end: number
+}
+
// Important: since this function uses Node.js only dependencies, it should
// always be used with a leading !__BROWSER__ check so that it can be
// tree-shaken from the browser build.
// fast path if expression is a simple identifier.
if (simpleIdRE.test(node.content)) {
if (!context.identifiers[node.content]) {
- node.content = `_ctx.${node.content}`
+ node.children = [`_ctx.`, createExpression(node.content, false, node.loc)]
}
return
}
context.onError(e)
return
}
- const ids: Node[] = []
+
+ const ids: (Identifier & PrefixMeta)[] = []
const knownIds = Object.create(context.identifiers)
+ // walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
walk(ast, {
- enter(node, parent) {
+ enter(node: Node & PrefixMeta, parent) {
if (node.type === 'Identifier') {
if (
ids.indexOf(node) === -1 &&
!knownIds[node.name] &&
shouldPrefix(node, parent)
) {
- node.name = `_ctx.${node.name}`
+ if (
+ parent.type === 'Property' &&
+ parent.value === node &&
+ parent.key === node
+ ) {
+ // property shorthand like { foo }, we need to add the key since we
+ // rewrite the value
+ node.prefix = `${node.name}: _ctx.`
+ } else {
+ node.prefix = `_ctx.`
+ }
ids.push(node)
}
} else if (isFunction(node)) {
+ // walk function expressions and add its arguments to known identifiers
+ // so that we don't prefix them
node.params.forEach(p =>
walk(p, {
enter(child) {
}
})
+ // We break up the coumpound expression into an array of strings and sub
+ // expressions (for identifiers that have been prefixed). In codegen, if
+ // an ExpressionNode has the `.children` property, it will be used instead of
+ // `.content`.
const full = node.content
const children: ExpressionNode['children'] = []
- ids.sort((a: any, b: any) => a.start - b.start)
- ids.forEach((id: any, i) => {
+ ids.sort((a, b) => a.start - b.start)
+ ids.forEach((id, i) => {
const last = ids[i - 1] as any
- const text = full.slice(last ? last.end - 1 : 0, id.start - 1)
- if (text.length) {
- children.push(text)
- }
- const source = full.slice(id.start, id.end)
+ const leadingText = full.slice(last ? last.end - 1 : 0, id.start - 1)
+ children.push(leadingText + id.prefix)
+ const source = full.slice(id.start - 1, id.end - 1)
children.push(
createExpression(id.name, false, {
source,
- start: advancePositionWithClone(node.loc.start, source, id.start),
- end: advancePositionWithClone(node.loc.start, source, id.end)
+ start: advancePositionWithClone(node.loc.start, source, id.start + 2),
+ end: advancePositionWithClone(node.loc.start, source, id.end + 2)
})
)
- if (i === ids.length - 1 && id.end < full.length - 1) {
+ if (i === ids.length - 1 && id.end - 1 < full.length) {
children.push(full.slice(id.end - 1))
}
})
.split(',')
)
-const isFunction = (node: Node): node is Function =>
- /Function(Expression|Declaration)$/.test(node.type)
-
function shouldPrefix(identifier: Identifier, parent: Node) {
if (
- // not id of a FunctionDeclaration
- !(parent.type === 'FunctionDeclaration' && parent.id === identifier) &&
- // not a params of a function
- !(isFunction(parent) && parent.params.indexOf(identifier) > -1) &&
+ !(
+ isFunction(parent) &&
+ // not id of a FunctionDeclaration
+ ((parent as any).id === identifier ||
+ // not a params of a function
+ parent.params.indexOf(identifier) > -1)
+ ) &&
// not a key of Property
!(
parent.type === 'Property' &&
parent.key === identifier &&
- !parent.computed
+ // computed keys should be prefixed
+ !parent.computed &&
+ // shorthand keys should be prefixed
+ !(parent.value === identifier)
) &&
// not a property of a MemberExpression
!(