utils.ts 15 KB

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