diff --git a/packages/compiler-core/__tests__/transforms/expression.spec.ts b/packages/compiler-core/__tests__/transforms/expression.spec.ts
index 2153fd08e..33b077440 100644
--- a/packages/compiler-core/__tests__/transforms/expression.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/expression.spec.ts
@@ -3,13 +3,16 @@ import { compile } from '../../src'
test(`should work`, async () => {
const { code, map } = compile(
- `
{{ ({ a }, b) => a + b + c }}
`,
+ `
+ {{ ({ a }, b) => a + b + i + c }} {{ i + 'fe' }} {{ i }}
+
+ {{ i }}
+ `,
{
useWith: false
}
)
console.log(code)
- console.log(map)
const consumer = await new SourceMapConsumer(map!)
const pos = consumer.originalPositionFor({
line: 4,
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index f66245adf..1a32b0e21 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -348,17 +348,14 @@ function genFor(node: ForNode, context: CodegenContext) {
genExpression(source, context)
push(`, (`)
if (valueAlias) {
- // not using genExpression here because these aliases can only be code
- // that is valid in the function argument position, so the parse rule can
- // be off and they don't need identifier prefixing anyway.
- push(valueAlias.content, valueAlias)
+ genExpression(valueAlias, context)
}
if (keyAlias) {
if (!valueAlias) {
push(`_`)
}
push(`, `)
- push(keyAlias.content, keyAlias)
+ genExpression(keyAlias, context)
}
if (objectIndexAlias) {
if (!keyAlias) {
@@ -369,7 +366,7 @@ function genFor(node: ForNode, context: CodegenContext) {
}
}
push(`, `)
- push(objectIndexAlias.content, objectIndexAlias)
+ genExpression(objectIndexAlias, context)
}
push(`) => `)
genChildren(children, context)
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 431feab45..14e89429a 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -2,7 +2,7 @@ import { SourceLocation } from './ast'
export interface CompilerError extends SyntaxError {
code: ErrorCodes
- loc: SourceLocation
+ loc?: SourceLocation
}
export function defaultOnError(error: CompilerError) {
@@ -11,13 +11,11 @@ export function defaultOnError(error: CompilerError) {
export function createCompilerError(
code: ErrorCodes,
- loc: SourceLocation
+ loc?: SourceLocation
): CompilerError {
- const error = new SyntaxError(
- `${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${
- loc.start.line
- }:${loc.start.column})`
- ) as CompilerError
+ const msg = __DEV__ || !__BROWSER__ ? errorMessages[code] : code
+ const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
+ const error = new SyntaxError(msg + locInfo) as CompilerError
error.code = code
error.loc = loc
return error
@@ -56,6 +54,8 @@ export const enum ErrorCodes {
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE,
+
+ // Vue-specific parse errors
X_INVALID_END_TAG,
X_MISSING_END_TAG,
X_MISSING_INTERPOLATION_END,
@@ -66,7 +66,10 @@ export const enum ErrorCodes {
X_ELSE_NO_ADJACENT_IF,
X_FOR_NO_EXPRESSION,
X_FOR_MALFORMED_EXPRESSION,
- X_V_BIND_NO_EXPRESSION
+ X_V_BIND_NO_EXPRESSION,
+
+ // generic errors
+ X_STRIP_WITH_NOT_SUPPORTED
}
export const errorMessages: { [code: number]: string } = {
@@ -116,14 +119,21 @@ export const errorMessages: { [code: number]: string } = {
"'' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
+
+ // Vue-specific parse errors
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.',
+ [ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
+ 'End bracket for dynamic directive argument was not found.',
// transform errors
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
- [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`
+ [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
+
+ // generic errors
+ [ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED]: `useWith: false is not supported in this build of compiler because it is optimized for payload size.`
}
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index a3a3e7117..0307f23ae 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -8,7 +8,8 @@ import { transformFor } from './transforms/vFor'
import { prepareElementForCodegen } from './transforms/element'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
-import { rewriteExpression } from './transforms/expression'
+import { expressionTransform } from './transforms/expression'
+import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@@ -17,13 +18,21 @@ export function compile(
options: CompilerOptions = {}
): CodegenResult {
const ast = isString(template) ? parse(template, options) : template
+ const useWith = __BROWSER__ || options.useWith !== false
+
+ if (__BROWSER__ && options.useWith === false) {
+ ;(options.onError || defaultOnError)(
+ createCompilerError(ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED)
+ )
+ }
transform(ast, {
...options,
+ useWith,
nodeTransforms: [
- ...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []),
transformIf,
transformFor,
+ ...(useWith ? [] : [expressionTransform]),
prepareElementForCodegen,
...(options.nodeTransforms || []) // user transforms
],
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index d3b442b38..862fce655 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -5,9 +5,10 @@ import {
ChildNode,
ElementNode,
DirectiveNode,
- Property
+ Property,
+ ExpressionNode
} from './ast'
-import { isString } from '@vue/shared'
+import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
// There are two types of transforms:
@@ -15,7 +16,10 @@ import { CompilerError, defaultOnError } from './errors'
// - NodeTransform:
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
// replace or remove the node being processed.
-export type NodeTransform = (node: ChildNode, context: TransformContext) => void
+export type NodeTransform = (
+ node: ChildNode,
+ context: TransformContext
+) => void | (() => void) | (() => void)[]
// - DirectiveTransform:
// Transforms that handles a single directive attribute on an element.
@@ -34,11 +38,12 @@ export type StructuralDirectiveTransform = (
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
-) => void
+) => void | (() => void)
export interface TransformOptions {
nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform }
+ useWith?: boolean
onError?: (error: CompilerError) => void
}
@@ -53,19 +58,27 @@ export interface TransformContext extends Required {
replaceNode(node: ChildNode): void
removeNode(node?: ChildNode): void
onNodeRemoved: () => void
+ addIdentifier(exp: ExpressionNode): void
+ removeIdentifier(exp: ExpressionNode): void
}
function createTransformContext(
root: RootNode,
- options: TransformOptions
+ {
+ useWith = true,
+ nodeTransforms = [],
+ directiveTransforms = {},
+ onError = defaultOnError
+ }: TransformOptions
): TransformContext {
const context: TransformContext = {
imports: new Set(),
statements: [],
identifiers: {},
- nodeTransforms: options.nodeTransforms || [],
- directiveTransforms: options.directiveTransforms || {},
- onError: options.onError || defaultOnError,
+ useWith,
+ nodeTransforms,
+ directiveTransforms,
+ onError,
parent: root,
ancestors: [],
childIndex: 0,
@@ -99,7 +112,13 @@ function createTransformContext(
}
context.parent.children.splice(removalIndex, 1)
},
- onNodeRemoved: () => {}
+ onNodeRemoved: () => {},
+ addIdentifier(exp) {
+ context.identifiers[exp.content] = true
+ },
+ removeIdentifier(exp) {
+ delete context.identifiers[exp.content]
+ }
}
return context
}
@@ -115,10 +134,7 @@ export function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
- // ancestors and identifiers need to be cached here since they may get
- // replaced during a child's traversal
const ancestors = context.ancestors.concat(parent)
- const identifiers = context.identifiers
let i = 0
const nodeRemoved = () => {
i--
@@ -131,7 +147,6 @@ export function traverseChildren(
context.ancestors = ancestors
context.childIndex = i
context.onNodeRemoved = nodeRemoved
- context.identifiers = identifiers
traverseNode(child, context)
}
}
@@ -139,9 +154,17 @@ export function traverseChildren(
export function traverseNode(node: ChildNode, context: TransformContext) {
// apply transform plugins
const { nodeTransforms } = context
+ const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
const plugin = nodeTransforms[i]
- plugin(node, context)
+ const onExit = plugin(node, context)
+ if (onExit) {
+ if (isArray(onExit)) {
+ exitFns.push(...onExit)
+ } else {
+ exitFns.push(onExit)
+ }
+ }
if (!context.currentNode) {
// node was removed
return
@@ -163,6 +186,11 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
traverseChildren(node, context)
break
}
+
+ // exit transforms
+ for (let i = 0; i < exitFns.length; i++) {
+ exitFns[i]()
+ }
}
export function createStructuralDirectiveTransform(
@@ -176,6 +204,7 @@ export function createStructuralDirectiveTransform(
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const { props } = node
+ const exitFns = []
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
@@ -184,9 +213,11 @@ export function createStructuralDirectiveTransform(
// traverse itself in case it moves the node around
props.splice(i, 1)
i--
- fn(node, prop, context)
+ const onExit = fn(node, prop, context)
+ if (onExit) exitFns.push(onExit)
}
}
+ return exitFns
}
}
}
diff --git a/packages/compiler-core/src/transforms/expression.ts b/packages/compiler-core/src/transforms/expression.ts
index d365091ab..3859b13a7 100644
--- a/packages/compiler-core/src/transforms/expression.ts
+++ b/packages/compiler-core/src/transforms/expression.ts
@@ -15,30 +15,56 @@ import { NodeTypes, createExpression, ExpressionNode } from '../ast'
import { Node, Function, Identifier } from 'estree'
import { advancePositionWithClone } from '../utils'
-export const rewriteExpression: NodeTransform = (node, context) => {
+export const expressionTransform: NodeTransform = (node, context) => {
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
- context.replaceNode(convertExpression(node, context))
+ processExpression(node, context)
} else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.exp) {
- prop.exp = convertExpression(prop.exp, context)
+ processExpression(prop.exp, context)
}
if (prop.arg && !prop.arg.isStatic) {
- prop.arg = convertExpression(prop.arg, context)
+ processExpression(prop.arg, context)
}
}
}
}
}
-function convertExpression(
+const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
+
+// cache node requires
+let _parseScript: typeof parseScript
+let _walk: typeof walk
+
+export function processExpression(
node: ExpressionNode,
context: TransformContext
-): ExpressionNode {
- const ast = parseScript(`(${node.content})`, { ranges: true }) as any
+) {
+ // lazy require dependencies so that they don't end up in rollup's dep graph
+ // and thus can be tree-shaken in browser builds.
+ const parseScript =
+ _parseScript || (_parseScript = require('meriyah').parseScript)
+ const walk = _walk || (_walk = require('estree-walker').walk)
+
+ // fast path if expression is a simple identifier.
+ if (simpleIdRE.test(node.content)) {
+ if (!context.identifiers[node.content]) {
+ node.content = `_ctx.${node.content}`
+ }
+ return
+ }
+
+ let ast
+ try {
+ ast = parseScript(`(${node.content})`, { ranges: true }) as any
+ } catch (e) {
+ context.onError(e)
+ return
+ }
const ids: Node[] = []
const knownIds = Object.create(context.identifiers)
@@ -98,10 +124,7 @@ function convertExpression(
}
})
- return {
- ...node,
- children
- }
+ node.children = children
}
const globals = new Set(
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index ed42fa698..9771e8590 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -1,8 +1,17 @@
-import { createStructuralDirectiveTransform } from '../transform'
-import { NodeTypes, ExpressionNode, createExpression } from '../ast'
+import {
+ createStructuralDirectiveTransform,
+ TransformContext
+} from '../transform'
+import {
+ NodeTypes,
+ ExpressionNode,
+ createExpression,
+ SourceLocation
+} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange } from '../utils'
import { RENDER_LIST } from '../runtimeConstants'
+import { processExpression } from './expression'
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
@@ -13,23 +22,35 @@ export const transformFor = createStructuralDirectiveTransform(
(node, dir, context) => {
if (dir.exp) {
context.imports.add(RENDER_LIST)
- const aliases = parseAliasExpressions(dir.exp.content)
+ const parseResult = parseForExpression(dir.exp, context)
+
+ if (parseResult) {
+ const { source, value, key, index } = parseResult
- if (aliases) {
- // TODO inject identifiers to context
- // and remove on exit
context.replaceNode({
type: NodeTypes.FOR,
loc: node.loc,
- source: maybeCreateExpression(
- aliases.source,
- dir.exp
- ) as ExpressionNode,
- valueAlias: maybeCreateExpression(aliases.value, dir.exp),
- keyAlias: maybeCreateExpression(aliases.key, dir.exp),
- objectIndexAlias: maybeCreateExpression(aliases.index, dir.exp),
+ source,
+ valueAlias: value,
+ keyAlias: key,
+ objectIndexAlias: index,
children: [node]
})
+
+ // scope management
+ const { addIdentifier, removeIdentifier } = context
+
+ // inject identifiers to context
+ value && addIdentifier(value)
+ key && addIdentifier(key)
+ index && addIdentifier(index)
+
+ return () => {
+ // remove injected identifiers on exit
+ value && removeIdentifier(value)
+ key && removeIdentifier(key)
+ index && removeIdentifier(index)
+ }
} else {
context.onError(
createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc)
@@ -43,28 +64,31 @@ export const transformFor = createStructuralDirectiveTransform(
}
)
-interface AliasExpression {
- offset: number
- content: string
+interface ForParseResult {
+ source: ExpressionNode
+ value: ExpressionNode | undefined
+ key: ExpressionNode | undefined
+ index: ExpressionNode | undefined
}
-interface AliasExpressions {
- source: AliasExpression
- value: AliasExpression | undefined
- key: AliasExpression | undefined
- index: AliasExpression | undefined
-}
-
-function parseAliasExpressions(source: string): AliasExpressions | null {
+function parseForExpression(
+ input: ExpressionNode,
+ context: TransformContext
+): ForParseResult | null {
+ const loc = input.loc
+ const source = input.content
const inMatch = source.match(forAliasRE)
if (!inMatch) return null
const [, LHS, RHS] = inMatch
- const result: AliasExpressions = {
- source: {
- offset: source.indexOf(RHS, LHS.length),
- content: RHS.trim()
- },
+ const result: ForParseResult = {
+ source: createAliasExpression(
+ loc,
+ RHS.trim(),
+ source.indexOf(RHS, LHS.length),
+ context,
+ !context.useWith
+ ),
value: undefined,
key: undefined,
index: undefined
@@ -80,49 +104,60 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
+ let keyOffset: number | undefined
if (keyContent) {
- result.key = {
- offset: source.indexOf(keyContent, trimmedOffset + valueContent.length),
- content: keyContent
- }
+ keyOffset = source.indexOf(
+ keyContent,
+ trimmedOffset + valueContent.length
+ )
+ result.key = createAliasExpression(loc, keyContent, keyOffset, context)
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
- result.index = {
- offset: source.indexOf(
+ result.index = createAliasExpression(
+ loc,
+ indexContent,
+ source.indexOf(
indexContent,
result.key
- ? result.key.offset + result.key.content.length
+ ? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
),
- content: indexContent
- }
+ context
+ )
}
}
}
if (valueContent) {
- result.value = {
- offset: trimmedOffset,
- content: valueContent
- }
+ result.value = createAliasExpression(
+ loc,
+ valueContent,
+ trimmedOffset,
+ context
+ )
}
return result
}
-function maybeCreateExpression(
- alias: AliasExpression | undefined,
- node: ExpressionNode
-): ExpressionNode | undefined {
- if (alias) {
- return createExpression(
- alias.content,
- false,
- getInnerRange(node.loc, alias.offset, alias.content.length)
- )
+function createAliasExpression(
+ range: SourceLocation,
+ content: string,
+ offset: number,
+ context: TransformContext,
+ process: boolean = false
+): ExpressionNode {
+ const exp = createExpression(
+ content,
+ false,
+ getInnerRange(range, offset, content.length)
+ )
+ if (!__BROWSER__ && process) {
+ processExpression(exp, context)
}
+ return exp
}
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index bcb0203d2..3bfa2341f 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -10,10 +10,14 @@ import {
IfBranchNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
+import { processExpression } from './expression'
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
+ if (!__BROWSER__ && !context.useWith && dir.exp) {
+ processExpression(dir.exp, context)
+ }
if (dir.name === 'if') {
context.replaceNode({
type: NodeTypes.IF,