utils.ts 14 KB

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