utils.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  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. BlockCodegenNode,
  26. MemoExpression
  27. } from './ast'
  28. import { TransformContext } from './transform'
  29. import {
  30. MERGE_PROPS,
  31. TELEPORT,
  32. SUSPENSE,
  33. KEEP_ALIVE,
  34. BASE_TRANSITION,
  35. TO_HANDLERS,
  36. NORMALIZE_PROPS,
  37. GUARD_REACTIVE_PROPS,
  38. WITH_MEMO
  39. } from './runtimeHelpers'
  40. import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
  41. import { PropsExpression } from './transforms/transformElement'
  42. import { parseExpression } from '@babel/parser'
  43. import { Expression } from '@babel/types'
  44. export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
  45. p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
  46. export const isBuiltInType = (tag: string, expected: string): boolean =>
  47. tag === expected || tag === hyphenate(expected)
  48. export function isCoreComponent(tag: string): symbol | void {
  49. if (isBuiltInType(tag, 'Teleport')) {
  50. return TELEPORT
  51. } else if (isBuiltInType(tag, 'Suspense')) {
  52. return SUSPENSE
  53. } else if (isBuiltInType(tag, 'KeepAlive')) {
  54. return KEEP_ALIVE
  55. } else if (isBuiltInType(tag, 'BaseTransition')) {
  56. return BASE_TRANSITION
  57. }
  58. }
  59. const nonIdentifierRE = /^\d|[^\$\w]/
  60. export const isSimpleIdentifier = (name: string): boolean =>
  61. !nonIdentifierRE.test(name)
  62. const enum MemberExpLexState {
  63. inMemberExp,
  64. inBrackets,
  65. inParens,
  66. inString
  67. }
  68. const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
  69. const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/
  70. const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
  71. /**
  72. * Simple lexer to check if an expression is a member expression. This is
  73. * lax and only checks validity at the root level (i.e. does not validate exps
  74. * inside square brackets), but it's ok since these are only used on template
  75. * expressions and false positives are invalid expressions in the first place.
  76. */
  77. export const isMemberExpressionBrowser = (path: string): boolean => {
  78. // remove whitespaces around . or [ first
  79. path = path.trim().replace(whitespaceRE, s => s.trim())
  80. let state = MemberExpLexState.inMemberExp
  81. let stateStack: MemberExpLexState[] = []
  82. let currentOpenBracketCount = 0
  83. let currentOpenParensCount = 0
  84. let currentStringType: "'" | '"' | '`' | null = null
  85. for (let i = 0; i < path.length; i++) {
  86. const char = path.charAt(i)
  87. switch (state) {
  88. case MemberExpLexState.inMemberExp:
  89. if (char === '[') {
  90. stateStack.push(state)
  91. state = MemberExpLexState.inBrackets
  92. currentOpenBracketCount++
  93. } else if (char === '(') {
  94. stateStack.push(state)
  95. state = MemberExpLexState.inParens
  96. currentOpenParensCount++
  97. } else if (
  98. !(i === 0 ? validFirstIdentCharRE : validIdentCharRE).test(char)
  99. ) {
  100. return false
  101. }
  102. break
  103. case MemberExpLexState.inBrackets:
  104. if (char === `'` || char === `"` || char === '`') {
  105. stateStack.push(state)
  106. state = MemberExpLexState.inString
  107. currentStringType = char
  108. } else if (char === `[`) {
  109. currentOpenBracketCount++
  110. } else if (char === `]`) {
  111. if (!--currentOpenBracketCount) {
  112. state = stateStack.pop()!
  113. }
  114. }
  115. break
  116. case MemberExpLexState.inParens:
  117. if (char === `'` || char === `"` || char === '`') {
  118. stateStack.push(state)
  119. state = MemberExpLexState.inString
  120. currentStringType = char
  121. } else if (char === `(`) {
  122. currentOpenParensCount++
  123. } else if (char === `)`) {
  124. // if the exp ends as a call then it should not be considered valid
  125. if (i === path.length - 1) {
  126. return false
  127. }
  128. if (!--currentOpenParensCount) {
  129. state = stateStack.pop()!
  130. }
  131. }
  132. break
  133. case MemberExpLexState.inString:
  134. if (char === currentStringType) {
  135. state = stateStack.pop()!
  136. currentStringType = null
  137. }
  138. break
  139. }
  140. }
  141. return !currentOpenBracketCount && !currentOpenParensCount
  142. }
  143. export const isMemberExpressionNode = __BROWSER__
  144. ? (NOOP as any as (path: string, context: TransformContext) => boolean)
  145. : (path: string, context: TransformContext): boolean => {
  146. try {
  147. let ret: Expression = parseExpression(path, {
  148. plugins: context.expressionPlugins
  149. })
  150. if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
  151. ret = ret.expression
  152. }
  153. return (
  154. ret.type === 'MemberExpression' ||
  155. ret.type === 'OptionalMemberExpression' ||
  156. ret.type === 'Identifier'
  157. )
  158. } catch (e) {
  159. return false
  160. }
  161. }
  162. export const isMemberExpression = __BROWSER__
  163. ? isMemberExpressionBrowser
  164. : isMemberExpressionNode
  165. export function getInnerRange(
  166. loc: SourceLocation,
  167. offset: number,
  168. length: number
  169. ): SourceLocation {
  170. __TEST__ && assert(offset <= loc.source.length)
  171. const source = loc.source.slice(offset, offset + length)
  172. const newLoc: SourceLocation = {
  173. source,
  174. start: advancePositionWithClone(loc.start, loc.source, offset),
  175. end: loc.end
  176. }
  177. if (length != null) {
  178. __TEST__ && assert(offset + length <= loc.source.length)
  179. newLoc.end = advancePositionWithClone(
  180. loc.start,
  181. loc.source,
  182. offset + length
  183. )
  184. }
  185. return newLoc
  186. }
  187. export function advancePositionWithClone(
  188. pos: Position,
  189. source: string,
  190. numberOfCharacters: number = source.length
  191. ): Position {
  192. return advancePositionWithMutation(
  193. extend({}, pos),
  194. source,
  195. numberOfCharacters
  196. )
  197. }
  198. // advance by mutation without cloning (for performance reasons), since this
  199. // gets called a lot in the parser
  200. export function advancePositionWithMutation(
  201. pos: Position,
  202. source: string,
  203. numberOfCharacters: number = source.length
  204. ): Position {
  205. let linesCount = 0
  206. let lastNewLinePos = -1
  207. for (let i = 0; i < numberOfCharacters; i++) {
  208. if (source.charCodeAt(i) === 10 /* newline char code */) {
  209. linesCount++
  210. lastNewLinePos = i
  211. }
  212. }
  213. pos.offset += numberOfCharacters
  214. pos.line += linesCount
  215. pos.column =
  216. lastNewLinePos === -1
  217. ? pos.column + numberOfCharacters
  218. : numberOfCharacters - lastNewLinePos
  219. return pos
  220. }
  221. export function assert(condition: boolean, msg?: string) {
  222. /* istanbul ignore if */
  223. if (!condition) {
  224. throw new Error(msg || `unexpected compiler condition`)
  225. }
  226. }
  227. export function findDir(
  228. node: ElementNode,
  229. name: string | RegExp,
  230. allowEmpty: boolean = false
  231. ): DirectiveNode | undefined {
  232. for (let i = 0; i < node.props.length; i++) {
  233. const p = node.props[i]
  234. if (
  235. p.type === NodeTypes.DIRECTIVE &&
  236. (allowEmpty || p.exp) &&
  237. (isString(name) ? p.name === name : name.test(p.name))
  238. ) {
  239. return p
  240. }
  241. }
  242. }
  243. export function findProp(
  244. node: ElementNode,
  245. name: string,
  246. dynamicOnly: boolean = false,
  247. allowEmpty: boolean = false
  248. ): ElementNode['props'][0] | undefined {
  249. for (let i = 0; i < node.props.length; i++) {
  250. const p = node.props[i]
  251. if (p.type === NodeTypes.ATTRIBUTE) {
  252. if (dynamicOnly) continue
  253. if (p.name === name && (p.value || allowEmpty)) {
  254. return p
  255. }
  256. } else if (
  257. p.name === 'bind' &&
  258. (p.exp || allowEmpty) &&
  259. isStaticArgOf(p.arg, name)
  260. ) {
  261. return p
  262. }
  263. }
  264. }
  265. export function isStaticArgOf(
  266. arg: DirectiveNode['arg'],
  267. name: string
  268. ): boolean {
  269. return !!(arg && isStaticExp(arg) && arg.content === name)
  270. }
  271. export function hasDynamicKeyVBind(node: ElementNode): boolean {
  272. return node.props.some(
  273. p =>
  274. p.type === NodeTypes.DIRECTIVE &&
  275. p.name === 'bind' &&
  276. (!p.arg || // v-bind="obj"
  277. p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
  278. !p.arg.isStatic) // v-bind:[foo]
  279. )
  280. }
  281. export function isText(
  282. node: TemplateChildNode
  283. ): node is TextNode | InterpolationNode {
  284. return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
  285. }
  286. export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
  287. return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
  288. }
  289. export function isTemplateNode(
  290. node: RootNode | TemplateChildNode
  291. ): node is TemplateNode {
  292. return (
  293. node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
  294. )
  295. }
  296. export function isSlotOutlet(
  297. node: RootNode | TemplateChildNode
  298. ): node is SlotOutletNode {
  299. return node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
  300. }
  301. const propsHelperSet = new Set([NORMALIZE_PROPS, GUARD_REACTIVE_PROPS])
  302. function getUnnormalizedProps(
  303. props: PropsExpression | '{}',
  304. callPath: CallExpression[] = []
  305. ): [PropsExpression | '{}', CallExpression[]] {
  306. if (
  307. props &&
  308. !isString(props) &&
  309. props.type === NodeTypes.JS_CALL_EXPRESSION
  310. ) {
  311. const callee = props.callee
  312. if (!isString(callee) && propsHelperSet.has(callee)) {
  313. return getUnnormalizedProps(
  314. props.arguments[0] as PropsExpression,
  315. callPath.concat(props)
  316. )
  317. }
  318. }
  319. return [props, callPath]
  320. }
  321. export function injectProp(
  322. node: VNodeCall | RenderSlotCall,
  323. prop: Property,
  324. context: TransformContext
  325. ) {
  326. let propsWithInjection: ObjectExpression | CallExpression | undefined
  327. /**
  328. * 1. mergeProps(...)
  329. * 2. toHandlers(...)
  330. * 3. normalizeProps(...)
  331. * 4. normalizeProps(guardReactiveProps(...))
  332. *
  333. * we need to get the real props before normalization
  334. */
  335. let props =
  336. node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
  337. let callPath: CallExpression[] = []
  338. let parentCall: CallExpression | undefined
  339. if (
  340. props &&
  341. !isString(props) &&
  342. props.type === NodeTypes.JS_CALL_EXPRESSION
  343. ) {
  344. const ret = getUnnormalizedProps(props)
  345. props = ret[0]
  346. callPath = ret[1]
  347. parentCall = callPath[callPath.length - 1]
  348. }
  349. if (props == null || isString(props)) {
  350. propsWithInjection = createObjectExpression([prop])
  351. } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
  352. // merged props... add ours
  353. // only inject key to object literal if it's the first argument so that
  354. // if doesn't override user provided keys
  355. const first = props.arguments[0] as string | JSChildNode
  356. if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  357. // #6631
  358. if (!hasProp(prop, first)) {
  359. first.properties.unshift(prop)
  360. }
  361. } else {
  362. if (props.callee === TO_HANDLERS) {
  363. // #2366
  364. propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
  365. createObjectExpression([prop]),
  366. props
  367. ])
  368. } else {
  369. props.arguments.unshift(createObjectExpression([prop]))
  370. }
  371. }
  372. !propsWithInjection && (propsWithInjection = props)
  373. } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  374. if (!hasProp(prop, props)) {
  375. props.properties.unshift(prop)
  376. }
  377. propsWithInjection = props
  378. } else {
  379. // single v-bind with expression, return a merged replacement
  380. propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
  381. createObjectExpression([prop]),
  382. props
  383. ])
  384. // in the case of nested helper call, e.g. `normalizeProps(guardReactiveProps(props))`,
  385. // it will be rewritten as `normalizeProps(mergeProps({ key: 0 }, props))`,
  386. // the `guardReactiveProps` will no longer be needed
  387. if (parentCall && parentCall.callee === GUARD_REACTIVE_PROPS) {
  388. parentCall = callPath[callPath.length - 2]
  389. }
  390. }
  391. if (node.type === NodeTypes.VNODE_CALL) {
  392. if (parentCall) {
  393. parentCall.arguments[0] = propsWithInjection
  394. } else {
  395. node.props = propsWithInjection
  396. }
  397. } else {
  398. if (parentCall) {
  399. parentCall.arguments[0] = propsWithInjection
  400. } else {
  401. node.arguments[2] = propsWithInjection
  402. }
  403. }
  404. }
  405. // check existing key to avoid overriding user provided keys
  406. function hasProp(prop: Property, props: ObjectExpression) {
  407. let result = false
  408. if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
  409. const propKeyName = prop.key.content
  410. result = props.properties.some(
  411. p =>
  412. p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
  413. p.key.content === propKeyName
  414. )
  415. }
  416. return result
  417. }
  418. export function toValidAssetId(
  419. name: string,
  420. type: 'component' | 'directive' | 'filter'
  421. ): string {
  422. // see issue#4422, we need adding identifier on validAssetId if variable `name` has specific character
  423. return `_${type}_${name.replace(/[^\w]/g, (searchValue, replaceValue) => {
  424. return searchValue === '-' ? '_' : name.charCodeAt(replaceValue).toString()
  425. })}`
  426. }
  427. // Check if a node contains expressions that reference current context scope ids
  428. export function hasScopeRef(
  429. node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
  430. ids: TransformContext['identifiers']
  431. ): boolean {
  432. if (!node || Object.keys(ids).length === 0) {
  433. return false
  434. }
  435. switch (node.type) {
  436. case NodeTypes.ELEMENT:
  437. for (let i = 0; i < node.props.length; i++) {
  438. const p = node.props[i]
  439. if (
  440. p.type === NodeTypes.DIRECTIVE &&
  441. (hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
  442. ) {
  443. return true
  444. }
  445. }
  446. return node.children.some(c => hasScopeRef(c, ids))
  447. case NodeTypes.FOR:
  448. if (hasScopeRef(node.source, ids)) {
  449. return true
  450. }
  451. return node.children.some(c => hasScopeRef(c, ids))
  452. case NodeTypes.IF:
  453. return node.branches.some(b => hasScopeRef(b, ids))
  454. case NodeTypes.IF_BRANCH:
  455. if (hasScopeRef(node.condition, ids)) {
  456. return true
  457. }
  458. return node.children.some(c => hasScopeRef(c, ids))
  459. case NodeTypes.SIMPLE_EXPRESSION:
  460. return (
  461. !node.isStatic &&
  462. isSimpleIdentifier(node.content) &&
  463. !!ids[node.content]
  464. )
  465. case NodeTypes.COMPOUND_EXPRESSION:
  466. return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
  467. case NodeTypes.INTERPOLATION:
  468. case NodeTypes.TEXT_CALL:
  469. return hasScopeRef(node.content, ids)
  470. case NodeTypes.TEXT:
  471. case NodeTypes.COMMENT:
  472. return false
  473. default:
  474. if (__DEV__) {
  475. const exhaustiveCheck: never = node
  476. exhaustiveCheck
  477. }
  478. return false
  479. }
  480. }
  481. export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
  482. if (node.type === NodeTypes.JS_CALL_EXPRESSION && node.callee === WITH_MEMO) {
  483. return node.arguments[1].returns as VNodeCall
  484. } else {
  485. return node
  486. }
  487. }