From e57cb5106624f4738e042a32bf23d3389ee9ed29 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 23 Sep 2019 13:25:18 -0400 Subject: [PATCH] feat(compiler): expression prefixing + v-for scope analysis --- .../__tests__/transforms/expression.spec.ts | 7 +- packages/compiler-core/src/codegen.ts | 9 +- packages/compiler-core/src/errors.ts | 28 ++-- packages/compiler-core/src/index.ts | 13 +- packages/compiler-core/src/transform.ts | 61 ++++++-- .../src/transforms/expression.ts | 45 ++++-- packages/compiler-core/src/transforms/vFor.ts | 139 +++++++++++------- packages/compiler-core/src/transforms/vIf.ts | 4 + 8 files changed, 209 insertions(+), 97 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/expression.spec.ts b/packages/compiler-core/__tests__/transforms/expression.spec.ts index 2153fd08..33b07744 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 f66245ad..1a32b0e2 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 431feab4..14e89429 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 } = { "' 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 d365091a..3859b13a 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 ed42fa69..9771e859 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 bcb0203d..3bfa2341 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,