feat(compiler): expression prefixing + v-for scope analysis
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user