utils.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import {
  2. SourceLocation,
  3. Position,
  4. ElementNode,
  5. NodeTypes,
  6. CallExpression,
  7. SequenceExpression,
  8. createSequenceExpression,
  9. createCallExpression,
  10. DirectiveNode,
  11. ElementTypes,
  12. TemplateChildNode,
  13. RootNode,
  14. ObjectExpression,
  15. Property,
  16. JSChildNode,
  17. createObjectExpression,
  18. SlotOutletNode,
  19. TemplateNode
  20. } from './ast'
  21. import { parse } from 'acorn'
  22. import { walk } from 'estree-walker'
  23. import { TransformContext } from './transform'
  24. import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeHelpers'
  25. import { isString, isFunction } from '@vue/shared'
  26. import { PropsExpression } from './transforms/transformElement'
  27. // cache node requires
  28. // lazy require dependencies so that they don't end up in rollup's dep graph
  29. // and thus can be tree-shaken in browser builds.
  30. let _parse: typeof parse
  31. let _walk: typeof walk
  32. export function loadDep(name: string) {
  33. if (typeof process !== 'undefined' && isFunction(require)) {
  34. return require(name)
  35. } else {
  36. // This is only used when we are building a dev-only build of the compiler
  37. // which runs in the browser but also uses Node deps.
  38. return (window as any)._deps[name]
  39. }
  40. }
  41. export const parseJS: typeof parse = (code: string, options: any) => {
  42. assert(
  43. !__BROWSER__,
  44. `Expression AST analysis can only be performed in non-browser builds.`
  45. )
  46. const parse = _parse || (_parse = loadDep('acorn').parse)
  47. return parse(code, options)
  48. }
  49. export const walkJS: typeof walk = (ast, walker) => {
  50. assert(
  51. !__BROWSER__,
  52. `Expression AST analysis can only be performed in non-browser builds.`
  53. )
  54. const walk = _walk || (_walk = loadDep('estree-walker').walk)
  55. return walk(ast, walker)
  56. }
  57. export const isSimpleIdentifier = (name: string): boolean =>
  58. !/^\d|[^\w]/.test(name)
  59. export function getInnerRange(
  60. loc: SourceLocation,
  61. offset: number,
  62. length?: number
  63. ): SourceLocation {
  64. __DEV__ && assert(offset <= loc.source.length)
  65. const source = loc.source.substr(offset, length)
  66. const newLoc: SourceLocation = {
  67. source,
  68. start: advancePositionWithClone(loc.start, loc.source, offset),
  69. end: loc.end
  70. }
  71. if (length != null) {
  72. __DEV__ && assert(offset + length <= loc.source.length)
  73. newLoc.end = advancePositionWithClone(
  74. loc.start,
  75. loc.source,
  76. offset + length
  77. )
  78. }
  79. return newLoc
  80. }
  81. export function advancePositionWithClone(
  82. pos: Position,
  83. source: string,
  84. numberOfCharacters: number = source.length
  85. ): Position {
  86. return advancePositionWithMutation({ ...pos }, source, numberOfCharacters)
  87. }
  88. // advance by mutation without cloning (for performance reasons), since this
  89. // gets called a lot in the parser
  90. export function advancePositionWithMutation(
  91. pos: Position,
  92. source: string,
  93. numberOfCharacters: number = source.length
  94. ): Position {
  95. let linesCount = 0
  96. let lastNewLinePos = -1
  97. for (let i = 0; i < numberOfCharacters; i++) {
  98. if (source.charCodeAt(i) === 10 /* newline char code */) {
  99. linesCount++
  100. lastNewLinePos = i
  101. }
  102. }
  103. pos.offset += numberOfCharacters
  104. pos.line += linesCount
  105. pos.column =
  106. lastNewLinePos === -1
  107. ? pos.column + numberOfCharacters
  108. : Math.max(1, numberOfCharacters - lastNewLinePos)
  109. return pos
  110. }
  111. export function assert(condition: boolean, msg?: string) {
  112. /* istanbul ignore if */
  113. if (!condition) {
  114. throw new Error(msg || `unexpected compiler condition`)
  115. }
  116. }
  117. export function findDir(
  118. node: ElementNode,
  119. name: string | RegExp,
  120. allowEmpty: boolean = false
  121. ): DirectiveNode | undefined {
  122. for (let i = 0; i < node.props.length; i++) {
  123. const p = node.props[i]
  124. if (
  125. p.type === NodeTypes.DIRECTIVE &&
  126. (allowEmpty || p.exp) &&
  127. (isString(name) ? p.name === name : name.test(p.name))
  128. ) {
  129. return p
  130. }
  131. }
  132. }
  133. export function findProp(
  134. node: ElementNode,
  135. name: string
  136. ): ElementNode['props'][0] | undefined {
  137. for (let i = 0; i < node.props.length; i++) {
  138. const p = node.props[i]
  139. if (p.type === NodeTypes.ATTRIBUTE) {
  140. if (p.name === name && p.value && !p.value.isEmpty) {
  141. return p
  142. }
  143. } else if (
  144. p.arg &&
  145. p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
  146. p.arg.isStatic &&
  147. p.arg.content === name &&
  148. p.exp
  149. ) {
  150. return p
  151. }
  152. }
  153. }
  154. export function createBlockExpression(
  155. args: CallExpression['arguments'],
  156. context: TransformContext
  157. ): SequenceExpression {
  158. return createSequenceExpression([
  159. createCallExpression(context.helper(OPEN_BLOCK)),
  160. createCallExpression(context.helper(CREATE_BLOCK), args)
  161. ])
  162. }
  163. export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
  164. p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
  165. export const isTemplateNode = (
  166. node: RootNode | TemplateChildNode
  167. ): node is TemplateNode =>
  168. node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
  169. export const isSlotOutlet = (
  170. node: RootNode | TemplateChildNode
  171. ): node is SlotOutletNode =>
  172. node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
  173. export function injectProp(
  174. props: PropsExpression | undefined | 'null',
  175. prop: Property,
  176. context: TransformContext
  177. ): ObjectExpression | CallExpression {
  178. if (props == null || props === `null`) {
  179. return createObjectExpression([prop])
  180. } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
  181. // merged props... add ours
  182. // only inject key to object literal if it's the first argument so that
  183. // if doesn't override user provided keys
  184. const first = props.arguments[0] as string | JSChildNode
  185. if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  186. first.properties.unshift(prop)
  187. } else {
  188. props.arguments.unshift(createObjectExpression([prop]))
  189. }
  190. return props
  191. } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
  192. props.properties.unshift(prop)
  193. return props
  194. } else {
  195. // single v-bind with expression, return a merged replacement
  196. return createCallExpression(context.helper(MERGE_PROPS), [
  197. createObjectExpression([prop]),
  198. props
  199. ])
  200. }
  201. }
  202. export function toValidAssetId(
  203. name: string,
  204. type: 'component' | 'directive'
  205. ): string {
  206. return `_${type}_${name.replace(/[^\w]/g, '')}`
  207. }