ssrTransformComponent.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import {
  2. NodeTransform,
  3. NodeTypes,
  4. ElementTypes,
  5. createCallExpression,
  6. resolveComponentType,
  7. buildProps,
  8. ComponentNode,
  9. SlotFnBuilder,
  10. createFunctionExpression,
  11. buildSlots,
  12. FunctionExpression,
  13. TemplateChildNode,
  14. createIfStatement,
  15. createSimpleExpression,
  16. getBaseTransformPreset,
  17. DOMNodeTransforms,
  18. DOMDirectiveTransforms,
  19. createReturnStatement,
  20. ReturnStatement,
  21. Namespaces,
  22. locStub,
  23. RootNode,
  24. TransformContext,
  25. CompilerOptions,
  26. TransformOptions,
  27. createRoot,
  28. createTransformContext,
  29. traverseNode,
  30. ExpressionNode,
  31. TemplateNode,
  32. SUSPENSE,
  33. TELEPORT,
  34. TRANSITION_GROUP,
  35. CREATE_VNODE,
  36. CallExpression
  37. } from '@vue/compiler-dom'
  38. import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
  39. import {
  40. SSRTransformContext,
  41. processChildren,
  42. processChildrenAsStatement
  43. } from '../ssrCodegenTransform'
  44. import { ssrProcessTeleport } from './ssrTransformTeleport'
  45. import {
  46. ssrProcessSuspense,
  47. ssrTransformSuspense
  48. } from './ssrTransformSuspense'
  49. import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
  50. import { isSymbol, isObject, isArray } from '@vue/shared'
  51. // We need to construct the slot functions in the 1st pass to ensure proper
  52. // scope tracking, but the children of each slot cannot be processed until
  53. // the 2nd pass, so we store the WIP slot functions in a weakmap during the 1st
  54. // pass and complete them in the 2nd pass.
  55. const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
  56. interface WIPSlotEntry {
  57. fn: FunctionExpression
  58. children: TemplateChildNode[]
  59. vnodeBranch: ReturnStatement
  60. }
  61. const componentTypeMap = new WeakMap<
  62. ComponentNode,
  63. string | symbol | CallExpression
  64. >()
  65. // ssr component transform is done in two phases:
  66. // In phase 1. we use `buildSlot` to analyze the children of the component into
  67. // WIP slot functions (it must be done in phase 1 because `buildSlot` relies on
  68. // the core transform context).
  69. // In phase 2. we convert the WIP slots from phase 1 into ssr-specific codegen
  70. // nodes.
  71. export const ssrTransformComponent: NodeTransform = (node, context) => {
  72. if (
  73. node.type !== NodeTypes.ELEMENT ||
  74. node.tagType !== ElementTypes.COMPONENT
  75. ) {
  76. return
  77. }
  78. const component = resolveComponentType(node, context, true /* ssr */)
  79. componentTypeMap.set(node, component)
  80. if (isSymbol(component)) {
  81. if (component === SUSPENSE) {
  82. return ssrTransformSuspense(node, context)
  83. }
  84. return // built-in component: fallthrough
  85. }
  86. // Build the fallback vnode-based branch for the component's slots.
  87. // We need to clone the node into a fresh copy and use the buildSlots' logic
  88. // to get access to the children of each slot. We then compile them with
  89. // a child transform pipeline using vnode-based transforms (instead of ssr-
  90. // based ones), and save the result branch (a ReturnStatement) in an array.
  91. // The branch is retrieved when processing slots again in ssr mode.
  92. const vnodeBranches: ReturnStatement[] = []
  93. const clonedNode = clone(node)
  94. return function ssrPostTransformComponent() {
  95. // Using the cloned node, build the normal VNode-based branches (for
  96. // fallback in case the child is render-fn based). Store them in an array
  97. // for later use.
  98. if (clonedNode.children.length) {
  99. buildSlots(clonedNode, context, (props, children) => {
  100. vnodeBranches.push(createVNodeSlotBranch(props, children, context))
  101. return createFunctionExpression(undefined)
  102. })
  103. }
  104. const props =
  105. node.props.length > 0
  106. ? // note we are not passing ssr: true here because for components, v-on
  107. // handlers should still be passed
  108. buildProps(node, context).props || `null`
  109. : `null`
  110. const wipEntries: WIPSlotEntry[] = []
  111. wipMap.set(node, wipEntries)
  112. const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
  113. const fn = createFunctionExpression(
  114. [props || `_`, `_push`, `_parent`, `_scopeId`],
  115. undefined, // no return, assign body later
  116. true, // newline
  117. true, // isSlot
  118. loc
  119. )
  120. wipEntries.push({
  121. fn,
  122. children,
  123. // also collect the corresponding vnode branch built earlier
  124. vnodeBranch: vnodeBranches[wipEntries.length]
  125. })
  126. return fn
  127. }
  128. const slots = node.children.length
  129. ? buildSlots(node, context, buildSSRSlotFn).slots
  130. : `null`
  131. if (typeof component !== 'string') {
  132. // dynamic component that resolved to a `resolveDynamicComponent` call
  133. // expression - since the resolved result may be a plain element (string)
  134. // or a VNode, handle it with `renderVNode`.
  135. node.ssrCodegenNode = createCallExpression(
  136. context.helper(SSR_RENDER_VNODE),
  137. [
  138. `_push`,
  139. createCallExpression(context.helper(CREATE_VNODE), [
  140. component,
  141. props,
  142. slots
  143. ]),
  144. `_parent`
  145. ]
  146. )
  147. } else {
  148. node.ssrCodegenNode = createCallExpression(
  149. context.helper(SSR_RENDER_COMPONENT),
  150. [component, props, slots, `_parent`]
  151. )
  152. }
  153. }
  154. }
  155. export function ssrProcessComponent(
  156. node: ComponentNode,
  157. context: SSRTransformContext
  158. ) {
  159. const component = componentTypeMap.get(node)!
  160. if (!node.ssrCodegenNode) {
  161. // this is a built-in component that fell-through.
  162. if (component === TELEPORT) {
  163. return ssrProcessTeleport(node, context)
  164. } else if (component === SUSPENSE) {
  165. return ssrProcessSuspense(node, context)
  166. } else if (component === TRANSITION_GROUP) {
  167. return ssrProcessTransitionGroup(node, context)
  168. } else {
  169. // real fall-through (e.g. KeepAlive): just render its children.
  170. processChildren(node.children, context)
  171. }
  172. } else {
  173. // finish up slot function expressions from the 1st pass.
  174. const wipEntries = wipMap.get(node) || []
  175. for (let i = 0; i < wipEntries.length; i++) {
  176. const { fn, children, vnodeBranch } = wipEntries[i]
  177. // For each slot, we generate two branches: one SSR-optimized branch and
  178. // one normal vnode-based branch. The branches are taken based on the
  179. // presence of the 2nd `_push` argument (which is only present if the slot
  180. // is called by `_ssrRenderSlot`.
  181. fn.body = createIfStatement(
  182. createSimpleExpression(`_push`, false),
  183. processChildrenAsStatement(
  184. children,
  185. context,
  186. false,
  187. true /* withSlotScopeId */
  188. ),
  189. vnodeBranch
  190. )
  191. }
  192. // component is inside a slot, inherit slot scope Id
  193. if (context.withSlotScopeId) {
  194. node.ssrCodegenNode!.arguments.push(`_scopeId`)
  195. }
  196. if (typeof component === 'string') {
  197. // static component
  198. context.pushStatement(
  199. createCallExpression(`_push`, [node.ssrCodegenNode])
  200. )
  201. } else {
  202. // dynamic component (`resolveDynamicComponent` call)
  203. // the codegen node is a `renderVNode` call
  204. context.pushStatement(node.ssrCodegenNode)
  205. }
  206. }
  207. }
  208. export const rawOptionsMap = new WeakMap<RootNode, CompilerOptions>()
  209. const [baseNodeTransforms, baseDirectiveTransforms] = getBaseTransformPreset(
  210. true
  211. )
  212. const vnodeNodeTransforms = [...baseNodeTransforms, ...DOMNodeTransforms]
  213. const vnodeDirectiveTransforms = {
  214. ...baseDirectiveTransforms,
  215. ...DOMDirectiveTransforms
  216. }
  217. function createVNodeSlotBranch(
  218. props: ExpressionNode | undefined,
  219. children: TemplateChildNode[],
  220. parentContext: TransformContext
  221. ): ReturnStatement {
  222. // apply a sub-transform using vnode-based transforms.
  223. const rawOptions = rawOptionsMap.get(parentContext.root)!
  224. const subOptions = {
  225. ...rawOptions,
  226. // overwrite with vnode-based transforms
  227. nodeTransforms: [
  228. ...vnodeNodeTransforms,
  229. ...(rawOptions.nodeTransforms || [])
  230. ],
  231. directiveTransforms: {
  232. ...vnodeDirectiveTransforms,
  233. ...(rawOptions.directiveTransforms || {})
  234. }
  235. }
  236. // wrap the children with a wrapper template for proper children treatment.
  237. const wrapperNode: TemplateNode = {
  238. type: NodeTypes.ELEMENT,
  239. ns: Namespaces.HTML,
  240. tag: 'template',
  241. tagType: ElementTypes.TEMPLATE,
  242. isSelfClosing: false,
  243. // important: provide v-slot="props" on the wrapper for proper
  244. // scope analysis
  245. props: [
  246. {
  247. type: NodeTypes.DIRECTIVE,
  248. name: 'slot',
  249. exp: props,
  250. arg: undefined,
  251. modifiers: [],
  252. loc: locStub
  253. }
  254. ],
  255. children,
  256. loc: locStub,
  257. codegenNode: undefined
  258. }
  259. subTransform(wrapperNode, subOptions, parentContext)
  260. return createReturnStatement(children)
  261. }
  262. function subTransform(
  263. node: TemplateChildNode,
  264. options: TransformOptions,
  265. parentContext: TransformContext
  266. ) {
  267. const childRoot = createRoot([node])
  268. const childContext = createTransformContext(childRoot, options)
  269. // this sub transform is for vnode fallback branch so it should be handled
  270. // like normal render functions
  271. childContext.ssr = false
  272. // inherit parent scope analysis state
  273. childContext.scopes = { ...parentContext.scopes }
  274. childContext.identifiers = { ...parentContext.identifiers }
  275. childContext.imports = parentContext.imports
  276. // traverse
  277. traverseNode(childRoot, childContext)
  278. // merge helpers/components/directives into parent context
  279. ;(['helpers', 'components', 'directives'] as const).forEach(key => {
  280. childContext[key].forEach((value: any, helperKey: any) => {
  281. if (key === 'helpers') {
  282. const parentCount = parentContext.helpers.get(helperKey)
  283. if (parentCount === undefined) {
  284. parentContext.helpers.set(helperKey, value)
  285. } else {
  286. parentContext.helpers.set(helperKey, value + parentCount)
  287. }
  288. } else {
  289. ;(parentContext[key] as any).add(value)
  290. }
  291. })
  292. })
  293. // imports/hoists are not merged because:
  294. // - imports are only used for asset urls and should be consistent between
  295. // node/client branches
  296. // - hoists are not enabled for the client branch here
  297. }
  298. function clone(v: any): any {
  299. if (isArray(v)) {
  300. return v.map(clone)
  301. } else if (isObject(v)) {
  302. const res: any = {}
  303. for (const key in v) {
  304. res[key] = clone(v[key])
  305. }
  306. return res
  307. } else {
  308. return v
  309. }
  310. }