feat(compiler): expression prefixing + v-for scope analysis

This commit is contained in:
Evan You 2019-09-23 13:25:18 -04:00
parent b04be6a561
commit e57cb51066
8 changed files with 209 additions and 97 deletions

View File

@ -3,13 +3,16 @@ import { compile } from '../../src'
test(`should work`, async () => { test(`should work`, async () => {
const { code, map } = compile( const { code, map } = compile(
`<div v-if="hello">{{ ({ a }, b) => a + b + c }}</div>`, `<div v-for="i in foo">
{{ ({ a }, b) => a + b + i + c }} {{ i + 'fe' }} {{ i }}
</div>
<p>{{ i }}</p>
`,
{ {
useWith: false useWith: false
} }
) )
console.log(code) console.log(code)
console.log(map)
const consumer = await new SourceMapConsumer(map!) const consumer = await new SourceMapConsumer(map!)
const pos = consumer.originalPositionFor({ const pos = consumer.originalPositionFor({
line: 4, line: 4,

View File

@ -348,17 +348,14 @@ function genFor(node: ForNode, context: CodegenContext) {
genExpression(source, context) genExpression(source, context)
push(`, (`) push(`, (`)
if (valueAlias) { if (valueAlias) {
// not using genExpression here because these aliases can only be code genExpression(valueAlias, context)
// 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)
} }
if (keyAlias) { if (keyAlias) {
if (!valueAlias) { if (!valueAlias) {
push(`_`) push(`_`)
} }
push(`, `) push(`, `)
push(keyAlias.content, keyAlias) genExpression(keyAlias, context)
} }
if (objectIndexAlias) { if (objectIndexAlias) {
if (!keyAlias) { if (!keyAlias) {
@ -369,7 +366,7 @@ function genFor(node: ForNode, context: CodegenContext) {
} }
} }
push(`, `) push(`, `)
push(objectIndexAlias.content, objectIndexAlias) genExpression(objectIndexAlias, context)
} }
push(`) => `) push(`) => `)
genChildren(children, context) genChildren(children, context)

View File

@ -2,7 +2,7 @@ import { SourceLocation } from './ast'
export interface CompilerError extends SyntaxError { export interface CompilerError extends SyntaxError {
code: ErrorCodes code: ErrorCodes
loc: SourceLocation loc?: SourceLocation
} }
export function defaultOnError(error: CompilerError) { export function defaultOnError(error: CompilerError) {
@ -11,13 +11,11 @@ export function defaultOnError(error: CompilerError) {
export function createCompilerError( export function createCompilerError(
code: ErrorCodes, code: ErrorCodes,
loc: SourceLocation loc?: SourceLocation
): CompilerError { ): CompilerError {
const error = new SyntaxError( const msg = __DEV__ || !__BROWSER__ ? errorMessages[code] : code
`${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${ const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
loc.start.line const error = new SyntaxError(msg + locInfo) as CompilerError
}:${loc.start.column})`
) as CompilerError
error.code = code error.code = code
error.loc = loc error.loc = loc
return error return error
@ -56,6 +54,8 @@ export const enum ErrorCodes {
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME, UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG, UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE, UNKNOWN_NAMED_CHARACTER_REFERENCE,
// Vue-specific parse errors
X_INVALID_END_TAG, X_INVALID_END_TAG,
X_MISSING_END_TAG, X_MISSING_END_TAG,
X_MISSING_INTERPOLATION_END, X_MISSING_INTERPOLATION_END,
@ -66,7 +66,10 @@ export const enum ErrorCodes {
X_ELSE_NO_ADJACENT_IF, X_ELSE_NO_ADJACENT_IF,
X_FOR_NO_EXPRESSION, X_FOR_NO_EXPRESSION,
X_FOR_MALFORMED_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 } = { export const errorMessages: { [code: number]: string } = {
@ -116,14 +119,21 @@ export const errorMessages: { [code: number]: string } = {
"'<?' is allowed only in XML context.", "'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.", [ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.', [ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
// Vue-specific parse errors
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.', [ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.', [ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]: [ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.', 'Interpolation end sign was not found.',
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
'End bracket for dynamic directive argument was not found.',
// transform errors // transform errors
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`, [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_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`, [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.`
} }

View File

@ -8,7 +8,8 @@ import { transformFor } from './transforms/vFor'
import { prepareElementForCodegen } from './transforms/element' import { prepareElementForCodegen } from './transforms/element'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind' 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 export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@ -17,13 +18,21 @@ export function compile(
options: CompilerOptions = {} options: CompilerOptions = {}
): CodegenResult { ): CodegenResult {
const ast = isString(template) ? parse(template, options) : template 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, { transform(ast, {
...options, ...options,
useWith,
nodeTransforms: [ nodeTransforms: [
...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []),
transformIf, transformIf,
transformFor, transformFor,
...(useWith ? [] : [expressionTransform]),
prepareElementForCodegen, prepareElementForCodegen,
...(options.nodeTransforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
], ],

View File

@ -5,9 +5,10 @@ import {
ChildNode, ChildNode,
ElementNode, ElementNode,
DirectiveNode, DirectiveNode,
Property Property,
ExpressionNode
} from './ast' } from './ast'
import { isString } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
// There are two types of transforms: // There are two types of transforms:
@ -15,7 +16,10 @@ import { CompilerError, defaultOnError } from './errors'
// - NodeTransform: // - NodeTransform:
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate, // Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
// replace or remove the node being processed. // 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: // - DirectiveTransform:
// Transforms that handles a single directive attribute on an element. // Transforms that handles a single directive attribute on an element.
@ -34,11 +38,12 @@ export type StructuralDirectiveTransform = (
node: ElementNode, node: ElementNode,
dir: DirectiveNode, dir: DirectiveNode,
context: TransformContext context: TransformContext
) => void ) => void | (() => void)
export interface TransformOptions { export interface TransformOptions {
nodeTransforms?: NodeTransform[] nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform } directiveTransforms?: { [name: string]: DirectiveTransform }
useWith?: boolean
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -53,19 +58,27 @@ export interface TransformContext extends Required<TransformOptions> {
replaceNode(node: ChildNode): void replaceNode(node: ChildNode): void
removeNode(node?: ChildNode): void removeNode(node?: ChildNode): void
onNodeRemoved: () => void onNodeRemoved: () => void
addIdentifier(exp: ExpressionNode): void
removeIdentifier(exp: ExpressionNode): void
} }
function createTransformContext( function createTransformContext(
root: RootNode, root: RootNode,
options: TransformOptions {
useWith = true,
nodeTransforms = [],
directiveTransforms = {},
onError = defaultOnError
}: TransformOptions
): TransformContext { ): TransformContext {
const context: TransformContext = { const context: TransformContext = {
imports: new Set(), imports: new Set(),
statements: [], statements: [],
identifiers: {}, identifiers: {},
nodeTransforms: options.nodeTransforms || [], useWith,
directiveTransforms: options.directiveTransforms || {}, nodeTransforms,
onError: options.onError || defaultOnError, directiveTransforms,
onError,
parent: root, parent: root,
ancestors: [], ancestors: [],
childIndex: 0, childIndex: 0,
@ -99,7 +112,13 @@ function createTransformContext(
} }
context.parent.children.splice(removalIndex, 1) context.parent.children.splice(removalIndex, 1)
}, },
onNodeRemoved: () => {} onNodeRemoved: () => {},
addIdentifier(exp) {
context.identifiers[exp.content] = true
},
removeIdentifier(exp) {
delete context.identifiers[exp.content]
}
} }
return context return context
} }
@ -115,10 +134,7 @@ export function traverseChildren(
parent: ParentNode, parent: ParentNode,
context: TransformContext 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 ancestors = context.ancestors.concat(parent)
const identifiers = context.identifiers
let i = 0 let i = 0
const nodeRemoved = () => { const nodeRemoved = () => {
i-- i--
@ -131,7 +147,6 @@ export function traverseChildren(
context.ancestors = ancestors context.ancestors = ancestors
context.childIndex = i context.childIndex = i
context.onNodeRemoved = nodeRemoved context.onNodeRemoved = nodeRemoved
context.identifiers = identifiers
traverseNode(child, context) traverseNode(child, context)
} }
} }
@ -139,9 +154,17 @@ export function traverseChildren(
export function traverseNode(node: ChildNode, context: TransformContext) { export function traverseNode(node: ChildNode, context: TransformContext) {
// apply transform plugins // apply transform plugins
const { nodeTransforms } = context const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) { for (let i = 0; i < nodeTransforms.length; i++) {
const plugin = nodeTransforms[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) { if (!context.currentNode) {
// node was removed // node was removed
return return
@ -163,6 +186,11 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
traverseChildren(node, context) traverseChildren(node, context)
break break
} }
// exit transforms
for (let i = 0; i < exitFns.length; i++) {
exitFns[i]()
}
} }
export function createStructuralDirectiveTransform( export function createStructuralDirectiveTransform(
@ -176,6 +204,7 @@ export function createStructuralDirectiveTransform(
return (node, context) => { return (node, context) => {
if (node.type === NodeTypes.ELEMENT) { if (node.type === NodeTypes.ELEMENT) {
const { props } = node const { props } = node
const exitFns = []
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
const prop = props[i] const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) { if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
@ -184,9 +213,11 @@ export function createStructuralDirectiveTransform(
// traverse itself in case it moves the node around // traverse itself in case it moves the node around
props.splice(i, 1) props.splice(i, 1)
i-- i--
fn(node, prop, context) const onExit = fn(node, prop, context)
if (onExit) exitFns.push(onExit)
} }
} }
return exitFns
} }
} }
} }

View File

@ -15,30 +15,56 @@ import { NodeTypes, createExpression, ExpressionNode } from '../ast'
import { Node, Function, Identifier } from 'estree' import { Node, Function, Identifier } from 'estree'
import { advancePositionWithClone } from '../utils' import { advancePositionWithClone } from '../utils'
export const rewriteExpression: NodeTransform = (node, context) => { export const expressionTransform: NodeTransform = (node, context) => {
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) { if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
context.replaceNode(convertExpression(node, context)) processExpression(node, context)
} else if (node.type === NodeTypes.ELEMENT) { } else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element // handle directives on element
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i] const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) { if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.exp) { if (prop.exp) {
prop.exp = convertExpression(prop.exp, context) processExpression(prop.exp, context)
} }
if (prop.arg && !prop.arg.isStatic) { 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, node: ExpressionNode,
context: TransformContext 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 ids: Node[] = []
const knownIds = Object.create(context.identifiers) const knownIds = Object.create(context.identifiers)
@ -98,10 +124,7 @@ function convertExpression(
} }
}) })
return { node.children = children
...node,
children
}
} }
const globals = new Set( const globals = new Set(

View File

@ -1,8 +1,17 @@
import { createStructuralDirectiveTransform } from '../transform' import {
import { NodeTypes, ExpressionNode, createExpression } from '../ast' createStructuralDirectiveTransform,
TransformContext
} from '../transform'
import {
NodeTypes,
ExpressionNode,
createExpression,
SourceLocation
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange } from '../utils' import { getInnerRange } from '../utils'
import { RENDER_LIST } from '../runtimeConstants' import { RENDER_LIST } from '../runtimeConstants'
import { processExpression } from './expression'
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
@ -13,23 +22,35 @@ export const transformFor = createStructuralDirectiveTransform(
(node, dir, context) => { (node, dir, context) => {
if (dir.exp) { if (dir.exp) {
context.imports.add(RENDER_LIST) 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({ context.replaceNode({
type: NodeTypes.FOR, type: NodeTypes.FOR,
loc: node.loc, loc: node.loc,
source: maybeCreateExpression( source,
aliases.source, valueAlias: value,
dir.exp keyAlias: key,
) as ExpressionNode, objectIndexAlias: index,
valueAlias: maybeCreateExpression(aliases.value, dir.exp),
keyAlias: maybeCreateExpression(aliases.key, dir.exp),
objectIndexAlias: maybeCreateExpression(aliases.index, dir.exp),
children: [node] 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 { } else {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc) createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc)
@ -43,28 +64,31 @@ export const transformFor = createStructuralDirectiveTransform(
} }
) )
interface AliasExpression { interface ForParseResult {
offset: number source: ExpressionNode
content: string value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
} }
interface AliasExpressions { function parseForExpression(
source: AliasExpression input: ExpressionNode,
value: AliasExpression | undefined context: TransformContext
key: AliasExpression | undefined ): ForParseResult | null {
index: AliasExpression | undefined const loc = input.loc
} const source = input.content
function parseAliasExpressions(source: string): AliasExpressions | null {
const inMatch = source.match(forAliasRE) const inMatch = source.match(forAliasRE)
if (!inMatch) return null if (!inMatch) return null
const [, LHS, RHS] = inMatch const [, LHS, RHS] = inMatch
const result: AliasExpressions = { const result: ForParseResult = {
source: { source: createAliasExpression(
offset: source.indexOf(RHS, LHS.length), loc,
content: RHS.trim() RHS.trim(),
}, source.indexOf(RHS, LHS.length),
context,
!context.useWith
),
value: undefined, value: undefined,
key: undefined, key: undefined,
index: undefined index: undefined
@ -80,49 +104,60 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
valueContent = valueContent.replace(forIteratorRE, '').trim() valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim() const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) { if (keyContent) {
result.key = { keyOffset = source.indexOf(
offset: source.indexOf(keyContent, trimmedOffset + valueContent.length), keyContent,
content: keyContent trimmedOffset + valueContent.length
} )
result.key = createAliasExpression(loc, keyContent, keyOffset, context)
} }
if (iteratorMatch[2]) { if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim() const indexContent = iteratorMatch[2].trim()
if (indexContent) { if (indexContent) {
result.index = { result.index = createAliasExpression(
offset: source.indexOf( loc,
indexContent,
source.indexOf(
indexContent, indexContent,
result.key result.key
? result.key.offset + result.key.content.length ? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length : trimmedOffset + valueContent.length
), ),
content: indexContent context
} )
} }
} }
} }
if (valueContent) { if (valueContent) {
result.value = { result.value = createAliasExpression(
offset: trimmedOffset, loc,
content: valueContent valueContent,
} trimmedOffset,
context
)
} }
return result return result
} }
function maybeCreateExpression( function createAliasExpression(
alias: AliasExpression | undefined, range: SourceLocation,
node: ExpressionNode content: string,
): ExpressionNode | undefined { offset: number,
if (alias) { context: TransformContext,
return createExpression( process: boolean = false
alias.content, ): ExpressionNode {
const exp = createExpression(
content,
false, false,
getInnerRange(node.loc, alias.offset, alias.content.length) getInnerRange(range, offset, content.length)
) )
if (!__BROWSER__ && process) {
processExpression(exp, context)
} }
return exp
} }

View File

@ -10,10 +10,14 @@ import {
IfBranchNode IfBranchNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './expression'
export const transformIf = createStructuralDirectiveTransform( export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
(node, dir, context) => { (node, dir, context) => {
if (!__BROWSER__ && !context.useWith && dir.exp) {
processExpression(dir.exp, context)
}
if (dir.name === 'if') { if (dir.name === 'if') {
context.replaceNode({ context.replaceNode({
type: NodeTypes.IF, type: NodeTypes.IF,