utils.ts 14 KB

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