// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: codegen callExpression + objectExpression + arrayExpression 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return createVNode(\\"div\\", {
id: \\"foo\\",
`;
exports[`compiler: codegen comment 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return createVNode(Comment, 0, \\"foo\\")
}
`;
exports[`compiler: codegen compound expression 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return toString(_ctx.foo)
}
`;
exports[`compiler: codegen forNode 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return renderList(list, (v, k, i) => toString(v))
}
`;
exports[`compiler: codegen forNode w/ skipped key alias 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return renderList(list, (v, __key, i) => toString(v))
}
`;
exports[`compiler: codegen forNode w/ skipped value alias 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return renderList(list, (__value, k, i) => toString(v))
}
`;
exports[`compiler: codegen forNode w/ skipped value and key aliases 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return renderList(list, (__value, __key, i) => toString(v))
}
}"
`;
+exports[`compiler: codegen hoists 1`] = `
+"const _hoisted_1 = hello
+const _hoisted_2 = { id: \\"foo\\" }
+
+return function render() {
+ with (this) {
+ return null
+ }
+}"
+`;
+
exports[`compiler: codegen ifNode 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return foo
? \\"foo\\"
`;
exports[`compiler: codegen ifNode with no v-else 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return foo
? \\"foo\\"
`;
exports[`compiler: codegen interpolation 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return toString(hello)
}
`;
exports[`compiler: codegen prefixIdentifiers: true should inject _ctx statement 1`] = `
-"return function render() {
+"
+return function render() {
const _ctx = this
return null
}"
`;
-exports[`compiler: codegen statement preambles 1`] = `
-"return function render() {
+exports[`compiler: codegen statements 1`] = `
+"
+return function render() {
const a = 1
const b = 2
`;
exports[`compiler: codegen static text 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return \\"hello\\"
}
`;
exports[`compiler: codegen text + comment + interpolation 1`] = `
-"return function render() {
+"
+return function render() {
with (this) {
return [
\\"foo\\",
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 3,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 2,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 2,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 4,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
"type": 1,
},
],
+ "hoists": Array [],
"imports": Array [],
"loc": Object {
"end": Object {
children: [],
imports: [],
statements: [],
+ hoists: [],
loc: mockLoc,
...options
}
expect(code).toMatchSnapshot()
})
- test('statement preambles', () => {
+ test('statements', () => {
const root = createRoot({
statements: [`const a = 1`, `const b = 2`]
})
expect(code).toMatchSnapshot()
})
+ test('hoists', () => {
+ const root = createRoot({
+ hoists: [
+ createExpression(`hello`, false, mockLoc),
+ createObjectExpression(
+ [
+ createObjectProperty(
+ createExpression(`id`, true, mockLoc),
+ createExpression(`foo`, true, mockLoc),
+ mockLoc
+ )
+ ],
+ mockLoc
+ )
+ ]
+ })
+ const { code } = generate(root)
+ expect(code).toMatch(`const _hoisted_1 = hello`)
+ expect(code).toMatch(`const _hoisted_2 = { id: "foo" }`)
+ expect(code).toMatchSnapshot()
+ })
+
test('prefixIdentifiers: true should inject _ctx statement', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true })
expect(code).toMatch(`const _ctx = this\n`)
import { parse } from '../src/parse'
import { transform, NodeTransform } from '../src/transform'
-import { ElementNode, NodeTypes } from '../src/ast'
+import {
+ ElementNode,
+ NodeTypes,
+ DirectiveNode,
+ ExpressionNode
+} from '../src/ast'
import { ErrorCodes, createCompilerError } from '../src/errors'
import { TO_STRING, CREATE_VNODE, COMMENT } from '../src/runtimeConstants'
expect(spy.mock.calls[1][0]).toBe(d1)
})
+ test('context.hoist', () => {
+ const ast = parse(`<div :id="foo"/><div :id="bar"/>`)
+ const hoisted: ExpressionNode[] = []
+ const mock: NodeTransform = (node, context) => {
+ const dir = (node as ElementNode).props[0] as DirectiveNode
+ hoisted.push(dir.exp!)
+ dir.exp = context.hoist(dir.exp!)
+ }
+ transform(ast, {
+ nodeTransforms: [mock]
+ })
+ expect(ast.hoists).toMatchObject(hoisted)
+ expect((ast as any).children[0].props[0].exp.content).toBe(`_hoisted_1`)
+ expect((ast as any).children[1].props[0].exp.content).toBe(`_hoisted_2`)
+ })
+
test('onError option', () => {
const ast = parse(`<div/>`)
const loc = ast.children[0].loc
})
test('static props', () => {
- const { node } = parseWithElementTransform(`<div id="foo" class="bar" />`)
+ const { root, node } = parseWithElementTransform(
+ `<div id="foo" class="bar" />`
+ )
expect(node.callee).toBe(CREATE_VNODE)
- expect(node.arguments).toMatchObject([
- `"div"`,
+ // should hoist the static object
+ expect(root.hoists).toMatchObject([
createStaticObjectMatcher({
id: 'foo',
class: 'bar'
})
])
+ expect(node.arguments).toMatchObject([
+ `"div"`,
+ {
+ type: NodeTypes.EXPRESSION,
+ content: `_hoisted_1`
+ }
+ ])
})
test('props + children', () => {
- const { node } = parseWithElementTransform(`<div id="foo"><span/></div>`)
+ const { root, node } = parseWithElementTransform(
+ `<div id="foo"><span/></div>`
+ )
expect(node.callee).toBe(CREATE_VNODE)
- expect(node.arguments).toMatchObject([
- `"div"`,
+ expect(root.hoists).toMatchObject([
createStaticObjectMatcher({
id: 'foo'
- }),
+ })
+ ])
+ expect(node.arguments).toMatchObject([
+ `"div"`,
+ {
+ type: NodeTypes.EXPRESSION,
+ content: `_hoisted_1`
+ },
[
{
type: NodeTypes.ELEMENT,
test('props dedupe', () => {
const { code } = compile(
- `<div class="a" :class="b" @click.foo="a" @click.bar="b" style="color: red" :style="{fontSize: 14}" />`
+ `<div class="a" :class="b" @click.foo="a" @click.bar="b" style="color: red" />
+ <div id="foo"/>`
)
console.log(code)
})
children: ChildNode[]
imports: string[]
statements: string[]
+ hoists: JSChildNode[]
}
export interface ElementNode extends Node {
if (mode === 'function') {
// generate const declarations for helpers
if (imports) {
- push(`const { ${imports} } = Vue\n\n`)
+ push(`const { ${imports} } = Vue\n`)
}
+ genHoists(ast.hoists, context)
push(`return `)
} else {
// generate import statements for helpers
if (imports) {
- push(`import { ${imports} } from 'vue'\n\n`)
+ push(`import { ${imports} } from 'vue'\n`)
}
+ genHoists(ast.hoists, context)
push(`export default `)
}
push(`function render() {`)
}
}
+function genHoists(hoists: JSChildNode[], context: CodegenContext) {
+ hoists.forEach((exp, i) => {
+ context.push(`const _hoisted_${i + 1} = `)
+ genNode(exp, context)
+ context.newline()
+ })
+ context.newline()
+}
+
// This will generate a single vnode call if:
// - The list has length === 1, AND:
// - This is a root node, OR:
import { transformBind } from './transforms/vBind'
import { transformExpression } from './transforms/transformExpression'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
+import { transformStyle } from './transforms/transformStyle'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
transformIf,
transformFor,
...(prefixIdentifiers ? [transformExpression] : []),
+ transformStyle,
transformElement,
...(options.nodeTransforms || []) // user transforms
],
createStructuralDirectiveTransform,
TransformOptions,
TransformContext,
- NodeTransform as Transform,
+ NodeTransform,
StructuralDirectiveTransform
} from './transform'
export {
} from './codegen'
export { ErrorCodes, CompilerError, createCompilerError } from './errors'
export * from './ast'
-
-// debug
-export {
- transformElement as prepareElementForCodegen
-} from './transforms/transformElement'
children: parseChildren(context, TextModes.DATA, []),
imports: [],
statements: [],
+ hoists: [],
loc: getSelection(context, start)
}
}
ElementNode,
DirectiveNode,
Property,
- ExpressionNode
+ ExpressionNode,
+ createExpression,
+ JSChildNode
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
root: RootNode
imports: Set<string>
statements: string[]
+ hoists: JSChildNode[]
identifiers: { [name: string]: number | undefined }
parent: ParentNode
childIndex: number
onNodeRemoved: () => void
addIdentifier(exp: ExpressionNode): void
removeIdentifier(exp: ExpressionNode): void
+ hoist(exp: JSChildNode): ExpressionNode
}
function createTransformContext(
root,
imports: new Set(),
statements: [],
+ hoists: [],
identifiers: {},
prefixIdentifiers,
nodeTransforms,
},
removeIdentifier({ content }) {
;(context.identifiers[content] as number)--
+ },
+ hoist(exp) {
+ context.hoists.push(exp)
+ return createExpression(
+ `_hoisted_${context.hoists.length}`,
+ false,
+ exp.loc
+ )
}
}
return context
traverseChildren(root, context)
root.imports = [...context.imports]
root.statements = context.statements
+ root.hoists = context.hoists
}
export function traverseChildren(
props: PropsExpression
directives: DirectiveNode[]
} {
+ let isStatic = true
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = []
)
} else {
// directives
+ isStatic = false
const { name, arg, exp, loc } = prop
// special case for v-bind and v-on with no argument
const isBind = name === 'bind'
)
}
+ // hoist the object if it's fully static
+ if (isStatic) {
+ propsExpression = context.hoist(propsExpression)
+ }
+
return {
props: propsExpression,
directives: runtimeDirectives
const name = prop.key.content
const existing = knownProps[name]
if (existing) {
- if (name.startsWith('on')) {
+ if (name.startsWith('on') || name === 'style') {
mergeAsArray(existing, prop)
- } else if (name === 'style') {
- mergeStyles(existing, prop)
} else if (name === 'class') {
mergeClasses(existing, prop)
}
}
}
-// Merge dynamic and static style into a single prop
-export function mergeStyles(existing: Property, incoming: Property) {
- if (
- existing.value.type === NodeTypes.JS_OBJECT_EXPRESSION &&
- incoming.value.type === NodeTypes.JS_OBJECT_EXPRESSION
- ) {
- // if both are objects, merge the object expressions.
- // style="color: red" :style="{ a: b }"
- // -> { color: "red", a: b }
- existing.value.properties.push(...incoming.value.properties)
- } else {
- // otherwise merge as array
- // style="color:red" :style="a"
- // -> style: [{ color: "red" }, a]
- mergeAsArray(existing, incoming)
- }
-}
-
// Merge dynamic and static class into a single prop
+// :class="expression" class="string"
+// -> class: expression + "string"
function mergeClasses(existing: Property, incoming: Property) {
const e = existing.value as ExpressionNode
const children =
children: undefined
}
])
- // :class="expression" class="string"
- // -> class: expression + "string"
children.push(` + " " + `, incoming.value as ExpressionNode)
}
if (prop.arg && !prop.arg.isStatic) {
if (prop.name === 'class') {
// TODO special expression optimization for classes
+ processExpression(prop.arg, context)
} else {
processExpression(prop.arg, context)
}
}
- } else if (prop.name === 'style') {
- // TODO parse inline CSS literals into objects
}
}
}
--- /dev/null
+import { NodeTransform } from '../transform'
+import { NodeTypes, createExpression } from '../ast'
+
+// prase inline CSS strings for static style attributes into an object
+export const transformStyle: NodeTransform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT) {
+ node.props.forEach((p, i) => {
+ if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
+ // replace p with an expression node
+ const parsed = JSON.stringify(parseInlineCSS(p.value.content))
+ const exp = context.hoist(createExpression(parsed, false, p.loc))
+ node.props[i] = {
+ type: NodeTypes.DIRECTIVE,
+ name: `bind`,
+ arg: createExpression(`style`, true, p.loc),
+ exp,
+ modifiers: [],
+ loc: p.loc
+ }
+ }
+ })
+ }
+}
+
+const listDelimiterRE = /;(?![^(]*\))/g
+const propertyDelimiterRE = /:(.+)/
+
+function parseInlineCSS(cssText: string): Record<string, string> {
+ const res: Record<string, string> = {}
+ cssText.split(listDelimiterRE).forEach(function(item) {
+ if (item) {
+ const tmp = item.split(propertyDelimiterRE)
+ tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
+ }
+ })
+ return res
+}