"children": Array [
Object {
"content": "a < b",
+ "isInterpolation": true,
"isStatic": false,
"loc": Object {
"end": Object {
"children": Array [
Object {
"content": "'</div>'",
+ "isInterpolation": true,
"isStatic": false,
"loc": Object {
"end": Object {
"children": Array [
Object {
"content": "",
+ "isInterpolation": true,
"isStatic": true,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "class",
+ "isInterpolation": false,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ some: condition }",
+ "isInterpolation": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "style",
+ "isInterpolation": false,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ color: 'red' }",
+ "isInterpolation": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "style",
+ "isInterpolation": false,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ color: 'red' }",
+ "isInterpolation": false,
"isStatic": false,
"loc": Object {
"end": Object {
Object {
"arg": Object {
"content": "class",
+ "isInterpolation": false,
"isStatic": true,
"loc": Object {
"end": Object {
},
"exp": Object {
"content": "{ some: condition }",
+ "isInterpolation": false,
"isStatic": false,
"loc": Object {
"end": Object {
with (this) {
return [
"hello ",
- world
+ toString(world)
]
}
}`
const consumer = await new SourceMapConsumer(map as RawSourceMap)
const pos = consumer.originalPositionFor({
line: 5,
- column: 6
+ column: 15
})
expect(pos).toMatchObject({
line: 1,
type: NodeTypes.EXPRESSION,
content: 'message',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 11, line: 1, column: 12 },
type: NodeTypes.EXPRESSION,
content: 'a<b',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
type: NodeTypes.EXPRESSION,
content: 'a<b',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
type: NodeTypes.EXPRESSION,
content: 'c>d',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 9, line: 1, column: 10 },
end: { offset: 18, line: 1, column: 19 },
type: NodeTypes.EXPRESSION,
content: '"</div>"',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 19, line: 1, column: 20 },
type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: false,
+ isInterpolation: false,
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 13, line: 1, column: 14 },
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'click',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'click',
start: {
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'click',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'click',
start: {
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'a',
start: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
+ isInterpolation: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'a',
start: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
+ isInterpolation: false,
loc: {
start: { offset: 13, line: 1, column: 14 },
end: { offset: 14, line: 1, column: 15 },
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'a',
start: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
+ isInterpolation: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
- type: 4,
+ type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: true,
+ isInterpolation: false,
loc: {
source: 'a',
start: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
+ isInterpolation: false,
loc: {
start: { offset: 14, line: 1, column: 15 },
end: { offset: 15, line: 1, column: 16 },
import { compile } from '../../src'
test(`should work`, async () => {
- const { code, map } = compile(
- `<div v-for="i in foo">
- {{ ({ a }, b) => a + b + i + c }} {{ i + 'fe' }} {{ i }}
- </div>
- <p>{{ i }}</p>
- `,
- {
- prefixIdentifiers: true
- }
- )
+ const { code, map } = compile(`<div>{{ foo }} bar</div>`, {
+ prefixIdentifiers: true
+ })
console.log(code)
const consumer = await new SourceMapConsumer(map!)
const pos = consumer.originalPositionFor({
type: NodeTypes.EXPRESSION
content: string
isStatic: boolean
+ isInterpolation: boolean
children?: (ExpressionNode | string)[]
}
type: NodeTypes.EXPRESSION,
loc,
content,
- isStatic
+ isStatic,
+ isInterpolation: false
}
}
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { advancePositionWithMutation, assert } from './utils'
import { isString, isArray } from '@vue/shared'
-import { RENDER_LIST } from './runtimeConstants'
+import { RENDER_LIST, TO_STRING } from './runtimeConstants'
type CodegenNode = ChildNode | JSChildNode
indent()
}
push(`return `)
- genChildren(ast.children, context)
+ genChildren(ast.children, context, true /* asRoot */)
if (!prefixIdentifiers) {
deindent()
push(`}`)
}
}
-// This will generate a single vnode call if the list has length === 1.
-function genChildren(children: ChildNode[], context: CodegenContext) {
- if (children.length === 1) {
- genNode(children[0], context)
+// This will generate a single vnode call if:
+// - The list has length === 1, AND:
+// - This is a root node, OR:
+// - The only child is a text or expression.
+function genChildren(
+ children: ChildNode[],
+ context: CodegenContext,
+ asRoot: boolean = false
+) {
+ const child = children[0]
+ if (
+ children.length === 1 &&
+ (asRoot ||
+ child.type === NodeTypes.TEXT ||
+ child.type == NodeTypes.EXPRESSION)
+ ) {
+ genNode(child, context)
} else {
genNodeListAsArray(children, context)
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
- // plain code string
- // note not adding quotes here because this can be any code,
- // not just plain strings.
push(node)
} else if (isArray(node)) {
- // child VNodes in a h() call
- // not using genChildren here because we want them to always be an array
- genNodeListAsArray(node, context)
+ genChildren(node, context)
} else {
genNode(node, context)
}
}
function genExpression(node: ExpressionNode, context: CodegenContext) {
- if (node.children) {
- return genCompoundExpression(node, context)
+ const { push } = context
+ const { content, children, isStatic, isInterpolation } = node
+ if (isInterpolation) {
+ push(`${TO_STRING}(`)
+ }
+ if (children) {
+ genCompoundExpression(node, context)
+ } else {
+ push(isStatic ? JSON.stringify(content) : content, node)
+ }
+ if (isInterpolation) {
+ push(`)`)
}
- const text = node.isStatic ? JSON.stringify(node.content) : node.content
- context.push(text, node)
}
function genExpressionAsPropertyKey(
type: NodeTypes.EXPRESSION,
content,
isStatic,
+ isInterpolation: false,
loc
}
}
type: NodeTypes.EXPRESSION,
content: value.content,
isStatic: false,
+ isInterpolation: false,
loc: value.loc
},
arg,
type: NodeTypes.EXPRESSION,
content,
loc: getSelection(context, start),
- isStatic: content === ''
+ isStatic: content === '',
+ isInterpolation: true
}
}
// Name mapping constants for runtime helpers that need to be imported in
// generated code. Make sure these are correctly exported in the runtime!
-export const CREATE_ELEMENT = `h`
+export const CREATE_VNODE = `createVNode`
export const RESOLVE_COMPONENT = `resolveComponent`
export const RESOLVE_DIRECTIVE = `resolveDirective`
export const APPLY_DIRECTIVES = `applyDirectives`
export const RENDER_LIST = `renderList`
export const CAPITALIZE = `capitalize`
+export const TO_STRING = `toString`
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
+import { TO_STRING } from './runtimeConstants'
// There are two types of transforms:
//
}
}
- // further traverse downwards
switch (node.type) {
+ case NodeTypes.EXPRESSION:
+ // no need to traverse, but we need to inject toString helper
+ if (node.isInterpolation) {
+ context.imports.add(TO_STRING)
+ }
+ break
+
+ // for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseChildren(node.branches[i], context)
import { isArray } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
- CREATE_ELEMENT,
+ CREATE_VNODE,
APPLY_DIRECTIVES,
RESOLVE_DIRECTIVE,
RESOLVE_COMPONENT
}
const { loc } = node
- context.imports.add(CREATE_ELEMENT)
- const vnode = createCallExpression(CREATE_ELEMENT, args, loc)
+ context.imports.add(CREATE_VNODE)
+ const vnode = createCallExpression(CREATE_VNODE, args, loc)
if (runtimeDirectives && runtimeDirectives.length) {
context.imports.add(APPLY_DIRECTIVES)
let _parseScript: typeof parseScript
let _walk: typeof walk
+// 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.
export function processExpression(
node: ExpressionNode,
context: TransformContext
if (node.type === 'Identifier') {
if (ids.indexOf(node) === -1) {
ids.push(node)
- if (!knownIds[node.name] && shouldPrependContext(node, parent)) {
+ if (!knownIds[node.name] && shouldPrefix(node, parent)) {
node.name = `_ctx.${node.name}`
}
}
const isFunction = (node: Node): node is Function =>
/Function(Expression|Declaration)$/.test(node.type)
-function shouldPrependContext(identifier: Identifier, parent: Node) {
+function shouldPrefix(identifier: Identifier, parent: Node) {
if (
// not id of a FunctionDeclaration
!(parent.type === 'FunctionDeclaration' && parent.id === identifier) &&
--- /dev/null
+// Optimizations
+// - b -> normalize(b)
+// - ['foo', b] -> 'foo' + normalize(b)
+// - { a, b: c } -> (a ? a : '') + (b ? c : '')
+// - ['a', b, { c }] -> 'a' + normalize(b) + (c ? c : '')
+
+// Also merge dynamic and static class into a single prop
+
+// Attach CLASS patchFlag if necessary
--- /dev/null
+// Optimizations
+// The compiler pre-compiles static string styles into static objects
+// + detects and hoists inline static objects
+
+// e.g. `style="color: red"` and `:style="{ color: 'red' }"` both get hoisted as
+
+// ``` js
+// const style = { color: 'red' }
+// render() { return e('div', { style }) }
+// ```
+
+// Also nerge dynamic and static style into a single prop
+
+// Attach STYLE patchFlag if necessary
'for',
(node, dir, context) => {
if (dir.exp) {
- context.imports.add(RENDER_LIST)
const parseResult = parseForExpression(dir.exp, context)
if (parseResult) {
+ context.imports.add(RENDER_LIST)
const { source, value, key, index } = parseResult
context.replaceNode({
type: NodeTypes.EXPRESSION,
content: 'a < b',
isStatic: false,
+ isInterpolation: true,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 19, line: 1, column: 20 },
-import { isObject } from '@vue/shared'
+import { isObject, toTypeString } from '@vue/shared'
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
import {
return (
!value._isVue &&
!value._isVNode &&
- observableValueRE.test(Object.prototype.toString.call(value)) &&
+ observableValueRE.test(toTypeString(value)) &&
!nonReactiveValues.has(value)
)
}
isArray,
isObject,
isReservedProp,
- hasOwn
+ hasOwn,
+ toTypeString
} from '@vue/shared'
import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component'
}
function toRawType(value: any): string {
- return Object.prototype.toString.call(value).slice(8, -1)
+ return toTypeString(value).slice(8, -1)
}
function isExplicable(type: string): boolean {
--- /dev/null
+import { isArray, isPlainObject, objectToString } from '@vue/shared'
+
+// for conversting {{ interpolation }} values to displayed strings.
+export function toString(val: any): string {
+ return val == null
+ ? ''
+ : isArray(val) || (isPlainObject(val) && val.toString === objectToString)
+ ? JSON.stringify(val, null, 2)
+ : String(val)
+}
} from './errorHandling'
// Internal, for compiler generated code
+// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
export { applyDirectives } from './directives'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
export { renderList } from './helpers/renderList'
+export { toString } from './helpers/toString'
export { capitalize } from '@vue/shared'
// Internal, for integration with runtime compiler
export const isObject = (val: any): val is Record<any, any> =>
val !== null && typeof val === 'object'
+export const objectToString = Object.prototype.toString
+export const toTypeString = (value: unknown): string =>
+ objectToString.call(value)
+
+export const isPlainObject = (val: any): val is object =>
+ toTypeString(val) === '[object Object]'
+
const vnodeHooksRE = /^vnode/
export const isReservedProp = (key: string): boolean =>
key === 'key' || key === 'ref' || vnodeHooksRE.test(key)
+import * as Vue from '../src'
import { createApp } from '../src'
+;(window as any).Vue = Vue
+
it('should support on-the-fly template compilation', () => {
const container = document.createElement('div')
const App = {