feat(compiler): ensure interpolation expressions are wrapped with toString()
This commit is contained in:
@@ -110,6 +110,7 @@ export interface ExpressionNode extends Node {
|
||||
type: NodeTypes.EXPRESSION
|
||||
content: string
|
||||
isStatic: boolean
|
||||
isInterpolation: boolean
|
||||
children?: (ExpressionNode | string)[]
|
||||
}
|
||||
|
||||
@@ -208,7 +209,8 @@ export function createExpression(
|
||||
type: NodeTypes.EXPRESSION,
|
||||
loc,
|
||||
content,
|
||||
isStatic
|
||||
isStatic,
|
||||
isInterpolation: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
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
|
||||
|
||||
@@ -149,7 +149,7 @@ export function generate(
|
||||
indent()
|
||||
}
|
||||
push(`return `)
|
||||
genChildren(ast.children, context)
|
||||
genChildren(ast.children, context, true /* asRoot */)
|
||||
if (!prefixIdentifiers) {
|
||||
deindent()
|
||||
push(`}`)
|
||||
@@ -162,10 +162,23 @@ export function generate(
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -192,14 +205,9 @@ function genNodeList(
|
||||
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)
|
||||
}
|
||||
@@ -264,11 +272,19 @@ function genText(node: TextNode | ExpressionNode, context: CodegenContext) {
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -523,6 +523,7 @@ function parseAttribute(
|
||||
type: NodeTypes.EXPRESSION,
|
||||
content,
|
||||
isStatic,
|
||||
isInterpolation: false,
|
||||
loc
|
||||
}
|
||||
}
|
||||
@@ -540,6 +541,7 @@ function parseAttribute(
|
||||
type: NodeTypes.EXPRESSION,
|
||||
content: value.content,
|
||||
isStatic: false,
|
||||
isInterpolation: false,
|
||||
loc: value.loc
|
||||
},
|
||||
arg,
|
||||
@@ -626,7 +628,8 @@ function parseInterpolation(
|
||||
type: NodeTypes.EXPRESSION,
|
||||
content,
|
||||
loc: getSelection(context, start),
|
||||
isStatic: content === ''
|
||||
isStatic: content === '',
|
||||
isInterpolation: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 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`
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from './ast'
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
import { CompilerError, defaultOnError } from './errors'
|
||||
import { TO_STRING } from './runtimeConstants'
|
||||
|
||||
// There are two types of transforms:
|
||||
//
|
||||
@@ -178,8 +179,15 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { isArray } from '@vue/shared'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
CREATE_ELEMENT,
|
||||
CREATE_VNODE,
|
||||
APPLY_DIRECTIVES,
|
||||
RESOLVE_DIRECTIVE,
|
||||
RESOLVE_COMPONENT
|
||||
@@ -67,8 +67,8 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -40,6 +40,9 @@ const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
|
||||
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
|
||||
@@ -73,7 +76,7 @@ export function processExpression(
|
||||
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}`
|
||||
}
|
||||
}
|
||||
@@ -141,7 +144,7 @@ const globals = new Set(
|
||||
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) &&
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// TODO
|
||||
@@ -1 +0,0 @@
|
||||
// TODO
|
||||
9
packages/compiler-core/src/transforms/vBindClass.ts
Normal file
9
packages/compiler-core/src/transforms/vBindClass.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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
|
||||
14
packages/compiler-core/src/transforms/vBindStyle.ts
Normal file
14
packages/compiler-core/src/transforms/vBindStyle.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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
|
||||
@@ -21,10 +21,10 @@ export const transformFor = createStructuralDirectiveTransform(
|
||||
'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({
|
||||
|
||||
Reference in New Issue
Block a user