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