utils.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import {
  2. SourceLocation,
  3. Position,
  4. ElementNode,
  5. NodeTypes,
  6. CallExpression,
  7. createCallExpression,
  8. DirectiveNode,
  9. ElementTypes,
  10. TemplateChildNode,
  11. RootNode,
  12. ObjectExpression,
  13. Property,
  14. JSChildNode,
  15. createObjectExpression,
  16. SlotOutletNode,
  17. TemplateNode,
  18. RenderSlotCall,
  19. ExpressionNode,
  20. IfBranchNode,
  21. TextNode,
  22. InterpolationNode,
  23. VNodeCall,
  24. SimpleExpressionNode
  25. } from './ast'
  26. import { TransformContext } from './transform'
  27. import {
  28. MERGE_PROPS,
  29. TELEPORT,
  30. SUSPENSE,
  31. KEEP_ALIVE,
  32. BASE_TRANSITION
  33. } from './runtimeHelpers'
  34. import { isString, isObject, hyphenate, extend } from '@vue/shared'
  35. export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
  36. p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
  37. export const isBuiltInType = (tag: string, expected: string): boolean =>
  38. tag === expected || tag === hyphenate(expected)
  39. export function isCoreComponent(tag: string): symbol | void {
  40. if (isBuiltInType(tag, 'Teleport')) {
  41. return TELEPORT
  42. } else if (isBuiltInType(tag, 'Suspense')) {
  43. return SUSPENSE
  44. } else if (isBuiltInType(tag, 'KeepAlive')) {
  45. return KEEP_ALIVE
  46. } else if (isBuiltInType(tag, 'BaseTransition')) {
  47. return BASE_TRANSITION
  48. }
  49. }
  50. const nonIdentifierRE = /^\d|[^\$\w]/
  51. export const isSimpleIdentifier = (name: string): boolean =>
  52. !nonIdentifierRE.test(name)
  53. const memberExpRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
  54. export const isMemberExpression = (path: string): boolean => {
  55. if (!path) return false
  56. return memberExpRE.test(path.trim())
  57. }
  58. export function getInnerRange(
  59. loc: SourceLocation,
  60. offset: number,
  61. length?: number
  62. ): SourceLocation {
  63. __TEST__ && assert(offset <= loc.source.length)
  64. const source = loc.source.substr(offset, length)
  65. const newLoc: SourceLocation = {
  66. source,
  67. start: advancePositionWithClone(loc.start, loc.source, offset),
  68. end: loc.end
  69. }
  70. if (length != null) {
  71. __TEST__ && assert(offset + length <= loc.source.length)
  72. newLoc.end = advancePositionWithClone(
  73. loc.start,
  74. loc.source,
  75. offset + length
  76. )
  77. }
  78. return newLoc
  79. }
  80. export function advancePositionWithClone(
  81. pos: Position,
  82. source: string,
  83. numberOfCharacters: number = source.length
  84. ): Position {
  85. return advancePositionWithMutation(
  86. extend({}, pos),
  87. source,
  88. numberOfCharacters
  89. )
  90. }
  91. // advance by mutation without cloning (for performance reasons), since this
  92. // gets called a lot in the parser
  93. export function advancePositionWithMutation(
  94. pos: Position,
  95. source: string,
  96. numberOfCharacters: number = source.length
  97. ): Position {
  98. let linesCount = 0
  99. let lastNewLinePos = -1
  100. for (let i = 0; i < numberOfCharacters; i++) {
  101. if (source.charCodeAt(i) === 10 /* newline char code */) {
  102. linesCount++
  103. lastNewLinePos = i
  104. }
  105. }
  106. pos.offset += numberOfCharacters
  107. pos.line += linesCount
  108. pos.column =
  109. lastNewLinePos === -1
  110. ? pos.column + numberOfCharacters
  111. : numberOfCharacters - lastNewLinePos
  112. return pos
  113. }
  114. export function assert(condition: boolean, msg?: string) {
  115. /* istanbul ignore if */
  116. if (!condition) {
  117. throw new Error(msg || `unexpected compiler condition`)
  118. }
  119. }
  120. export function findDir(
  121. node: ElementNode,
  122. name: string | RegExp,
  123. allowEmpty: boolean = false
  124. ): DirectiveNode | undefined {
  125. for (let i = 0; i < node.props.length; i++) {
  126. const p = node.props[i]
  127. if (
  128. p.type === NodeTypes.DIRECTIVE &&
  129. (allowEmpty || p.exp) &&
  130. (isString(name) ? p.name === name : name.test(p.name))
  131. ) {
  132. return p
  133. }
  134. }
  135. }
  136. export function findProp(
  137. node: ElementNode,
  138. name: string,
  139. dynamicOnly: boolean = false,
  140. allowEmpty: boolean = false
  141. ): ElementNode['props'][0] | undefined {
  142. for (let i = 0; i < node.props.length; i++) {
  143. const p = node.props[i]
  144. if (p.type === NodeTypes.ATTRIBUTE) {
  145. if (dynamicOnly) continue
  146. if (p.name === name && (p.value || allowEmpty)) {
  147. return p
  148. }
  149. } else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {
  150. return p
  151. }
  152. }
  153. }
  154. export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
  155. return !!(arg && isStaticExp(arg) && arg.content === name)
  156. }
  157. export function hasDynamicKeyVBind(node: ElementNode): boolean {
  158. return node.props.some(
  159. p =>
  160. p.type === NodeTypes.DIRECTIVE &&
  161. p.name === 'bind' &&
  162. (!p.arg || // v-bind="obj"
  163. p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
  164. !p.arg.isStatic) // v-bind:[foo]
  165. )
  166. }
  167. export function isText(
  168. node: TemplateChildNode
  169. ): node is TextNode | InterpolationNode {
  170. return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
  171. }
  172. export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
  173. return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
  174. }
  175. export function isTemplateNode(
  176. node: RootNode | TemplateChildNode
  177. ): node is TemplateNode {
  178. return (
  179. node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
  180. )
  181. }
  182. export function isSlotOutlet(
  183. node: RootNode | TemplateChildNode
  184. ): node is SlotOutletNode {
  185. return node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
  186. }
  187. export function injectProp(
  188. node: VNodeCall | RenderSlotCall,
  189. prop: Property,
  190. context: TransformContext
  191. ) {
  192. let propsWithInjection: ObjectExpression | CallExpression
  193. const props =
  194. node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
  195. if (props == null || isString(props)) {
  196. propsWithInjection = createObjectExpression([prop])
  197. } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
  198. // merged props... add ours
  199. // only inject key to object literal if it's the first argument so that
  200. // if doesn't override user provided keys
  201. const first = props.arguments[0] as string | JSChildNode
  202. if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  203. first.properties.unshift(prop)
  204. } else {
  205. props.arguments.unshift(createObjectExpression([prop]))
  206. }
  207. propsWithInjection = props
  208. } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  209. let alreadyExists = false
  210. // check existing key to avoid overriding user provided keys
  211. if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
  212. const propKeyName = prop.key.content
  213. alreadyExists = props.properties.some(
  214. p =>
  215. p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
  216. p.key.content === propKeyName
  217. )
  218. }
  219. if (!alreadyExists) {
  220. props.properties.unshift(prop)
  221. }
  222. propsWithInjection = props
  223. } else {
  224. // single v-bind with expression, return a merged replacement
  225. propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
  226. createObjectExpression([prop]),
  227. props
  228. ])
  229. }
  230. if (node.type === NodeTypes.VNODE_CALL) {
  231. node.props = propsWithInjection
  232. } else {
  233. node.arguments[2] = propsWithInjection
  234. }
  235. }
  236. export function toValidAssetId(
  237. name: string,
  238. type: 'component' | 'directive'
  239. ): string {
  240. return `_${type}_${name.replace(/[^\w]/g, '_')}`
  241. }
  242. // Check if a node contains expressions that reference current context scope ids
  243. export function hasScopeRef(
  244. node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
  245. ids: TransformContext['identifiers']
  246. ): boolean {
  247. if (!node || Object.keys(ids).length === 0) {
  248. return false
  249. }
  250. switch (node.type) {
  251. case NodeTypes.ELEMENT:
  252. for (let i = 0; i < node.props.length; i++) {
  253. const p = node.props[i]
  254. if (
  255. p.type === NodeTypes.DIRECTIVE &&
  256. (hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
  257. ) {
  258. return true
  259. }
  260. }
  261. return node.children.some(c => hasScopeRef(c, ids))
  262. case NodeTypes.FOR:
  263. if (hasScopeRef(node.source, ids)) {
  264. return true
  265. }
  266. return node.children.some(c => hasScopeRef(c, ids))
  267. case NodeTypes.IF:
  268. return node.branches.some(b => hasScopeRef(b, ids))
  269. case NodeTypes.IF_BRANCH:
  270. if (hasScopeRef(node.condition, ids)) {
  271. return true
  272. }
  273. return node.children.some(c => hasScopeRef(c, ids))
  274. case NodeTypes.SIMPLE_EXPRESSION:
  275. return (
  276. !node.isStatic &&
  277. isSimpleIdentifier(node.content) &&
  278. !!ids[node.content]
  279. )
  280. case NodeTypes.COMPOUND_EXPRESSION:
  281. return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
  282. case NodeTypes.INTERPOLATION:
  283. case NodeTypes.TEXT_CALL:
  284. return hasScopeRef(node.content, ids)
  285. case NodeTypes.TEXT:
  286. case NodeTypes.COMMENT:
  287. return false
  288. default:
  289. if (__DEV__) {
  290. const exhaustiveCheck: never = node
  291. exhaustiveCheck
  292. }
  293. return false
  294. }
  295. }