utils.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. TO_HANDLERS
  34. } from './runtimeHelpers'
  35. import { isString, isObject, hyphenate, extend } from '@vue/shared'
  36. export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
  37. p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
  38. export const isBuiltInType = (tag: string, expected: string): boolean =>
  39. tag === expected || tag === hyphenate(expected)
  40. export function isCoreComponent(tag: string): symbol | void {
  41. if (isBuiltInType(tag, 'Teleport')) {
  42. return TELEPORT
  43. } else if (isBuiltInType(tag, 'Suspense')) {
  44. return SUSPENSE
  45. } else if (isBuiltInType(tag, 'KeepAlive')) {
  46. return KEEP_ALIVE
  47. } else if (isBuiltInType(tag, 'BaseTransition')) {
  48. return BASE_TRANSITION
  49. }
  50. }
  51. const nonIdentifierRE = /^\d|[^\$\w]/
  52. export const isSimpleIdentifier = (name: string): boolean =>
  53. !nonIdentifierRE.test(name)
  54. const enum MemberExpLexState {
  55. inMemberExp,
  56. inBrackets,
  57. inString
  58. }
  59. const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
  60. const validIdentCharRE = /[\.\w$\xA0-\uFFFF]/
  61. const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
  62. /**
  63. * Simple lexer to check if an expression is a member expression. This is
  64. * lax and only checks validity at the root level (i.e. does not validate exps
  65. * inside square brackets), but it's ok since these are only used on template
  66. * expressions and false positives are invalid expressions in the first place.
  67. */
  68. export const isMemberExpression = (path: string): boolean => {
  69. // remove whitespaces around . or [ first
  70. path = path.trim().replace(whitespaceRE, s => s.trim())
  71. let state = MemberExpLexState.inMemberExp
  72. let prevState = MemberExpLexState.inMemberExp
  73. let currentOpenBracketCount = 0
  74. let currentStringType: "'" | '"' | '`' | null = null
  75. for (let i = 0; i < path.length; i++) {
  76. const char = path.charAt(i)
  77. switch (state) {
  78. case MemberExpLexState.inMemberExp:
  79. if (char === '[') {
  80. prevState = state
  81. state = MemberExpLexState.inBrackets
  82. currentOpenBracketCount++
  83. } else if (
  84. !(i === 0 ? validFirstIdentCharRE : validIdentCharRE).test(char)
  85. ) {
  86. return false
  87. }
  88. break
  89. case MemberExpLexState.inBrackets:
  90. if (char === `'` || char === `"` || char === '`') {
  91. prevState = state
  92. state = MemberExpLexState.inString
  93. currentStringType = char
  94. } else if (char === `[`) {
  95. currentOpenBracketCount++
  96. } else if (char === `]`) {
  97. if (!--currentOpenBracketCount) {
  98. state = prevState
  99. }
  100. }
  101. break
  102. case MemberExpLexState.inString:
  103. if (char === currentStringType) {
  104. state = prevState
  105. currentStringType = null
  106. }
  107. break
  108. }
  109. }
  110. return !currentOpenBracketCount
  111. }
  112. export function getInnerRange(
  113. loc: SourceLocation,
  114. offset: number,
  115. length?: number
  116. ): SourceLocation {
  117. __TEST__ && assert(offset <= loc.source.length)
  118. const source = loc.source.substr(offset, length)
  119. const newLoc: SourceLocation = {
  120. source,
  121. start: advancePositionWithClone(loc.start, loc.source, offset),
  122. end: loc.end
  123. }
  124. if (length != null) {
  125. __TEST__ && assert(offset + length <= loc.source.length)
  126. newLoc.end = advancePositionWithClone(
  127. loc.start,
  128. loc.source,
  129. offset + length
  130. )
  131. }
  132. return newLoc
  133. }
  134. export function advancePositionWithClone(
  135. pos: Position,
  136. source: string,
  137. numberOfCharacters: number = source.length
  138. ): Position {
  139. return advancePositionWithMutation(
  140. extend({}, pos),
  141. source,
  142. numberOfCharacters
  143. )
  144. }
  145. // advance by mutation without cloning (for performance reasons), since this
  146. // gets called a lot in the parser
  147. export function advancePositionWithMutation(
  148. pos: Position,
  149. source: string,
  150. numberOfCharacters: number = source.length
  151. ): Position {
  152. let linesCount = 0
  153. let lastNewLinePos = -1
  154. for (let i = 0; i < numberOfCharacters; i++) {
  155. if (source.charCodeAt(i) === 10 /* newline char code */) {
  156. linesCount++
  157. lastNewLinePos = i
  158. }
  159. }
  160. pos.offset += numberOfCharacters
  161. pos.line += linesCount
  162. pos.column =
  163. lastNewLinePos === -1
  164. ? pos.column + numberOfCharacters
  165. : numberOfCharacters - lastNewLinePos
  166. return pos
  167. }
  168. export function assert(condition: boolean, msg?: string) {
  169. /* istanbul ignore if */
  170. if (!condition) {
  171. throw new Error(msg || `unexpected compiler condition`)
  172. }
  173. }
  174. export function findDir(
  175. node: ElementNode,
  176. name: string | RegExp,
  177. allowEmpty: boolean = false
  178. ): DirectiveNode | undefined {
  179. for (let i = 0; i < node.props.length; i++) {
  180. const p = node.props[i]
  181. if (
  182. p.type === NodeTypes.DIRECTIVE &&
  183. (allowEmpty || p.exp) &&
  184. (isString(name) ? p.name === name : name.test(p.name))
  185. ) {
  186. return p
  187. }
  188. }
  189. }
  190. export function findProp(
  191. node: ElementNode,
  192. name: string,
  193. dynamicOnly: boolean = false,
  194. allowEmpty: boolean = false
  195. ): ElementNode['props'][0] | undefined {
  196. for (let i = 0; i < node.props.length; i++) {
  197. const p = node.props[i]
  198. if (p.type === NodeTypes.ATTRIBUTE) {
  199. if (dynamicOnly) continue
  200. if (p.name === name && (p.value || allowEmpty)) {
  201. return p
  202. }
  203. } else if (
  204. p.name === 'bind' &&
  205. (p.exp || allowEmpty) &&
  206. isBindKey(p.arg, name)
  207. ) {
  208. return p
  209. }
  210. }
  211. }
  212. export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
  213. return !!(arg && isStaticExp(arg) && arg.content === name)
  214. }
  215. export function hasDynamicKeyVBind(node: ElementNode): boolean {
  216. return node.props.some(
  217. p =>
  218. p.type === NodeTypes.DIRECTIVE &&
  219. p.name === 'bind' &&
  220. (!p.arg || // v-bind="obj"
  221. p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
  222. !p.arg.isStatic) // v-bind:[foo]
  223. )
  224. }
  225. export function isText(
  226. node: TemplateChildNode
  227. ): node is TextNode | InterpolationNode {
  228. return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
  229. }
  230. export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
  231. return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
  232. }
  233. export function isTemplateNode(
  234. node: RootNode | TemplateChildNode
  235. ): node is TemplateNode {
  236. return (
  237. node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
  238. )
  239. }
  240. export function isSlotOutlet(
  241. node: RootNode | TemplateChildNode
  242. ): node is SlotOutletNode {
  243. return node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
  244. }
  245. export function injectProp(
  246. node: VNodeCall | RenderSlotCall,
  247. prop: Property,
  248. context: TransformContext
  249. ) {
  250. let propsWithInjection: ObjectExpression | CallExpression | undefined
  251. const props =
  252. node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
  253. if (props == null || isString(props)) {
  254. propsWithInjection = createObjectExpression([prop])
  255. } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
  256. // merged props... add ours
  257. // only inject key to object literal if it's the first argument so that
  258. // if doesn't override user provided keys
  259. const first = props.arguments[0] as string | JSChildNode
  260. if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  261. first.properties.unshift(prop)
  262. } else {
  263. if (props.callee === TO_HANDLERS) {
  264. // #2366
  265. propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
  266. createObjectExpression([prop]),
  267. props
  268. ])
  269. } else {
  270. props.arguments.unshift(createObjectExpression([prop]))
  271. }
  272. }
  273. !propsWithInjection && (propsWithInjection = props)
  274. } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  275. let alreadyExists = false
  276. // check existing key to avoid overriding user provided keys
  277. if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
  278. const propKeyName = prop.key.content
  279. alreadyExists = props.properties.some(
  280. p =>
  281. p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
  282. p.key.content === propKeyName
  283. )
  284. }
  285. if (!alreadyExists) {
  286. props.properties.unshift(prop)
  287. }
  288. propsWithInjection = props
  289. } else {
  290. // single v-bind with expression, return a merged replacement
  291. propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
  292. createObjectExpression([prop]),
  293. props
  294. ])
  295. }
  296. if (node.type === NodeTypes.VNODE_CALL) {
  297. node.props = propsWithInjection
  298. } else {
  299. node.arguments[2] = propsWithInjection
  300. }
  301. }
  302. export function toValidAssetId(
  303. name: string,
  304. type: 'component' | 'directive' | 'filter'
  305. ): string {
  306. return `_${type}_${name.replace(/[^\w]/g, '_')}`
  307. }
  308. // Check if a node contains expressions that reference current context scope ids
  309. export function hasScopeRef(
  310. node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
  311. ids: TransformContext['identifiers']
  312. ): boolean {
  313. if (!node || Object.keys(ids).length === 0) {
  314. return false
  315. }
  316. switch (node.type) {
  317. case NodeTypes.ELEMENT:
  318. for (let i = 0; i < node.props.length; i++) {
  319. const p = node.props[i]
  320. if (
  321. p.type === NodeTypes.DIRECTIVE &&
  322. (hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
  323. ) {
  324. return true
  325. }
  326. }
  327. return node.children.some(c => hasScopeRef(c, ids))
  328. case NodeTypes.FOR:
  329. if (hasScopeRef(node.source, ids)) {
  330. return true
  331. }
  332. return node.children.some(c => hasScopeRef(c, ids))
  333. case NodeTypes.IF:
  334. return node.branches.some(b => hasScopeRef(b, ids))
  335. case NodeTypes.IF_BRANCH:
  336. if (hasScopeRef(node.condition, ids)) {
  337. return true
  338. }
  339. return node.children.some(c => hasScopeRef(c, ids))
  340. case NodeTypes.SIMPLE_EXPRESSION:
  341. return (
  342. !node.isStatic &&
  343. isSimpleIdentifier(node.content) &&
  344. !!ids[node.content]
  345. )
  346. case NodeTypes.COMPOUND_EXPRESSION:
  347. return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
  348. case NodeTypes.INTERPOLATION:
  349. case NodeTypes.TEXT_CALL:
  350. return hasScopeRef(node.content, ids)
  351. case NodeTypes.TEXT:
  352. case NodeTypes.COMMENT:
  353. return false
  354. default:
  355. if (__DEV__) {
  356. const exhaustiveCheck: never = node
  357. exhaustiveCheck
  358. }
  359. return false
  360. }
  361. }