vue3-yuanma/packages/compiler-core/src/utils.ts

253 lines
6.8 KiB
TypeScript
Raw Normal View History

import {
SourceLocation,
Position,
ElementNode,
NodeTypes,
CallExpression,
SequenceExpression,
createSequenceExpression,
createCallExpression,
DirectiveNode,
ElementTypes,
TemplateChildNode,
RootNode,
ObjectExpression,
Property,
JSChildNode,
createObjectExpression,
SlotOutletNode,
TemplateNode,
BlockCodegenNode,
ElementCodegenNode,
SlotOutletCodegenNode,
ComponentCodegenNode,
ExpressionNode
} from './ast'
2019-10-02 23:05:56 +08:00
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import { OPEN_BLOCK, MERGE_PROPS, RENDER_SLOT } from './runtimeHelpers'
2019-10-05 01:08:06 +08:00
import { isString, isFunction } from '@vue/shared'
// cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph
// and thus can be tree-shaken in browser builds.
2019-10-02 23:05:56 +08:00
let _parse: typeof parse
let _walk: typeof walk
2019-10-05 05:43:20 +08:00
export function loadDep(name: string) {
2019-10-05 01:08:06 +08:00
if (typeof process !== 'undefined' && isFunction(require)) {
return require(name)
} else {
// This is only used when we are building a dev-only build of the compiler
// which runs in the browser but also uses Node deps.
return (window as any)._deps[name]
}
}
2019-10-13 07:33:48 +08:00
export const parseJS: typeof parse = (code, options) => {
assert(
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
)
2019-10-05 01:08:06 +08:00
const parse = _parse || (_parse = loadDep('acorn').parse)
return parse(code, options)
}
export const walkJS: typeof walk = (ast, walker) => {
assert(
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
)
2019-10-05 01:08:06 +08:00
const walk = _walk || (_walk = loadDep('estree-walker').walk)
return walk(ast, walker)
}
const nonIdentifierRE = /^\d|[^\$\w]/
2019-09-25 10:39:20 +08:00
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
const memberExpRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
export const isMemberExpression = (path: string): boolean =>
memberExpRE.test(path)
2019-09-25 10:39:20 +08:00
export function getInnerRange(
loc: SourceLocation,
offset: number,
length?: number
): SourceLocation {
__DEV__ && assert(offset <= loc.source.length)
const source = loc.source.substr(offset, length)
const newLoc: SourceLocation = {
source,
start: advancePositionWithClone(loc.start, loc.source, offset),
end: loc.end
}
if (length != null) {
__DEV__ && assert(offset + length <= loc.source.length)
newLoc.end = advancePositionWithClone(
loc.start,
loc.source,
offset + length
)
}
return newLoc
}
export function advancePositionWithClone(
pos: Position,
source: string,
numberOfCharacters: number = source.length
): Position {
return advancePositionWithMutation({ ...pos }, source, numberOfCharacters)
}
// advance by mutation without cloning (for performance reasons), since this
// gets called a lot in the parser
export function advancePositionWithMutation(
pos: Position,
source: string,
numberOfCharacters: number = source.length
): Position {
let linesCount = 0
let lastNewLinePos = -1
for (let i = 0; i < numberOfCharacters; i++) {
if (source.charCodeAt(i) === 10 /* newline char code */) {
linesCount++
lastNewLinePos = i
}
}
pos.offset += numberOfCharacters
pos.line += linesCount
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters
: Math.max(1, numberOfCharacters - lastNewLinePos)
return pos
}
export function assert(condition: boolean, msg?: string) {
2019-09-25 04:35:01 +08:00
/* istanbul ignore if */
if (!condition) {
2019-09-23 04:50:57 +08:00
throw new Error(msg || `unexpected compiler condition`)
}
}
2019-10-03 06:03:42 +08:00
export function findDir(
node: ElementNode,
2019-10-03 06:03:42 +08:00
name: string | RegExp,
allowEmpty: boolean = false
): DirectiveNode | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
2019-10-03 06:03:42 +08:00
(allowEmpty || p.exp) &&
(isString(name) ? p.name === name : name.test(p.name))
) {
return p
}
}
}
export function findProp(
node: ElementNode,
name: string,
dynamicOnly: boolean = false
): ElementNode['props'][0] | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (dynamicOnly) continue
if (p.name === name && p.value && !p.value.isEmpty) {
return p
}
} else if (
p.name === 'bind' &&
p.arg &&
p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
p.arg.isStatic &&
p.arg.content === name &&
p.exp
) {
return p
}
}
}
export function createBlockExpression(
blockExp: BlockCodegenNode,
context: TransformContext
): SequenceExpression {
return createSequenceExpression([
createCallExpression(context.helper(OPEN_BLOCK)),
blockExp
])
}
export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
export const isTemplateNode = (
node: RootNode | TemplateChildNode
): node is TemplateNode =>
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
export const isSlotOutlet = (
node: RootNode | TemplateChildNode
): node is SlotOutletNode =>
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
export function injectProp(
node: ElementCodegenNode | ComponentCodegenNode | SlotOutletCodegenNode,
prop: Property,
context: TransformContext
) {
let propsWithInjection: ObjectExpression | CallExpression
const props =
node.callee === RENDER_SLOT ? node.arguments[2] : node.arguments[1]
if (props == null || isString(props)) {
propsWithInjection = createObjectExpression([prop])
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
// merged props... add ours
// only inject key to object literal if it's the first argument so that
// if doesn't override user provided keys
const first = props.arguments[0] as string | JSChildNode
if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
first.properties.unshift(prop)
} else {
props.arguments.unshift(createObjectExpression([prop]))
}
propsWithInjection = props
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
props.properties.unshift(prop)
propsWithInjection = props
} else {
// single v-bind with expression, return a merged replacement
propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
createObjectExpression([prop]),
props
])
}
if (node.callee === RENDER_SLOT) {
node.arguments[2] = propsWithInjection
} else {
node.arguments[1] = propsWithInjection
}
}
export function toValidAssetId(
name: string,
type: 'component' | 'directive'
): string {
return `_${type}_${name.replace(/[^\w]/g, '_')}`
}
export function isEmptyExpression(node: ExpressionNode) {
return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim()
}