@hippy/vue-next-server-renderer
Version:
Vue-Next server renderer for Hippy native framework
971 lines (962 loc) • 37.2 kB
JavaScript
/*!
* @hippy/vue-next-server-renderer v3.3.2
* (Using Vue v3.4.32 and Hippy-Vue-Next v3.3.2)
* Build at: Fri Nov 08 2024 20:45:13 GMT+0800 (中国标准时间)
*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2023-2024 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getCurrentInstance, warn, Fragment, Comment, Static, Text, mergeProps, ssrContextKey, ssrUtils, createVNode } from '@vue/runtime-core';
import { renderToString, ssrRenderSlotInner } from '@vue/server-renderer';
export { pipeToWebWritable, renderToNodeStream, renderToSimpleStream, renderToString, renderToWebStream, ssrGetDynamicModelProps, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderDynamicAttr, ssrRenderDynamicModel, ssrRenderList, ssrRenderSuspense, ssrRenderTeleport } from '@vue/server-renderer';
import { isString, normalizeStyle, ShapeFlags, isPromise, isFunction, NOOP, isArray } from '@vue/shared';
export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared';
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* remove unnecessary punctuation in node string, and parse node string to object
*/
function getObjectNodeList(nodeString, rootNode) {
// flat nested array and connect
// for example [
// '{"id":22,"index":0,"name":"View","tagName":"div","props":{},"children":[',
// [ '{"id":23,"index":0,"name":"View","tagName":"div","props":{},},' ],
// '],},'
// ]
// transform to
// ['{"id":22,"index":0,"name":"View","tagName":"div","props":{},"children":
// [','{"id":23,"index":0,"name":"View","tagName":"div","props":{},},', '],},']
const rawString = nodeString.flat(Infinity).join('');
// remove unnecessary punctuation
let parsedStr = rawString
.replace(/,}|,]/g, match => match.slice(1))
.replace(/,$/, '')
.replace(/\n/g, '\\n');
let ssrNodeTree;
try {
if (rootNode) {
const rootNodeStr = JSON.stringify(rootNode);
parsedStr = rootNodeStr.replace('"children":[]', `"children":[${parsedStr}]`);
}
// parse json string to json object
ssrNodeTree = JSON.parse(parsedStr);
return ssrNodeTree;
}
catch (e) {
return null;
}
}
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// native text input type
const INPUT_VALUE_MAP = {
number: 'numeric',
text: 'default',
search: 'web-search',
};
/**
* merge default props to ssr node. some native node should have default props, so we need to add
* to prop. the same logic with client runtime
*
* @param node - ssrNode
* @param nodeList - ssrNodeList
*/
function mergeDefaultNativeProps(node, nodeList) {
const commonProps = { id: '', class: '' };
let defaultNativeProps = {};
switch (node.name) {
case 'ListView':
defaultNativeProps = {
// calculate child nums
numberOfRows: nodeList.filter(v => v.pId === node.id).length,
};
break;
case 'Text':
defaultNativeProps = { text: '' };
break;
case 'TextInput':
defaultNativeProps = { underlineColorAndroid: 0 };
if (node.tagName === 'textarea') {
defaultNativeProps.numberOfLines = 5;
}
break;
case 'ViewPager':
defaultNativeProps = { initialPage: node.props.current };
break;
case 'WebView':
defaultNativeProps = {
method: 'get',
userAgent: '',
};
break;
case 'Modal':
defaultNativeProps = {
transparent: true,
immersionStatusBar: true,
collapsable: false,
};
break;
case 'Image':
defaultNativeProps = {
backgroundColor: 0,
};
break;
}
return Object.assign(commonProps, defaultNativeProps);
}
/**
* parse text input map props
*
* @param tagName
* @param rawProps
*/
function parseTextInputProps(tagName, rawProps) {
var _a;
const props = rawProps;
// handle common input props map
if (props.type) {
props.keyboardType = (_a = INPUT_VALUE_MAP[props.type]) !== null && _a !== void 0 ? _a : props.type;
delete props.type;
}
if (props.disabled) {
props.editable = !props.disabled;
delete props.disabled;
}
if (typeof props.value !== 'undefined') {
props.defaultValue = props.value;
delete props.value;
}
if (props.maxlength) {
props.maxLength = props.maxlength;
delete props.maxlength;
}
if (tagName === 'textarea') {
props.multiline = true;
// handle textarea props map
if (props.rows) {
props.numberOfLines = props.rows;
delete props.rows;
}
}
else {
props.numberOfLines = 1;
props.multiline = false;
}
return props;
}
/**
* get node's props. merge all props need to merged
*
* @param node - ssr node
* @param nodeList - ssr node list
* @param isIOS - client is iOS or not
*/
function getNodeProps(node, nodeList, isIOS) {
var _a, _b, _c, _d, _e, _f;
let { props } = node;
// merge all props
props = {
...mergeDefaultNativeProps(node, nodeList),
...props,
...props.mergedProps,
};
delete props.mergedProps;
// assign id and class to attribute props. id & class should use for devtools at client side
// and use for ssr
props.attributes = {
id: (_b = (_a = props === null || props === void 0 ? void 0 : props.attributes) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : props.id,
class: (_d = (_c = props === null || props === void 0 ? void 0 : props.attributes) === null || _c === void 0 ? void 0 : _c.class) !== null && _d !== void 0 ? _d : props.class,
};
// delete unnecessary props
delete props.id;
delete props.class;
delete props.attributes.style;
delete props.attributes.text;
// compatible iOS image src and fontWeight
if (isIOS && node.name === 'Image' && props.src) {
props.source = [{ uri: props.src }];
delete props.src;
}
// compatible fontWeight
if ((_e = props === null || props === void 0 ? void 0 : props.style) === null || _e === void 0 ? void 0 : _e.fontWeight) {
props.style.fontWeight = String(props.style.fontWeight);
}
// compatible placeholder
if (typeof props.placeholder !== 'undefined') {
props.placeholder = String(props.placeholder);
}
if (node.name === 'TextInput') {
parseTextInputProps((_f = node.tagName) !== null && _f !== void 0 ? _f : '', props);
}
if (node.name === 'WebView') {
props.source = {
uri: props.src,
};
delete props.src;
}
return props;
}
/**
* convert ssr node's special kebabCase props name to camelCase name
*
* @param rawProps
*/
function convertSpecialKebabCaseToCamelCase(rawProps) {
const props = rawProps;
if (props['caret-color']) {
props.caretColor = props['caret-color'];
delete props['caret-color'];
}
if (props['underline-color-android']) {
props.underlineColorAndroid = props['underline-color-android'];
delete props['underline-color-android'];
}
if (props['placeholder-text-color']) {
props.placeholderTextColor = props['placeholder-text-color'];
delete props['placeholder-text-color'];
}
if (props['break-strategy']) {
props.breakStrategy = props['break-strategy'];
delete props['break-strategy'];
}
return props;
}
/**
* convert ssr node list to hippy node list, add props for every node
*
* @param nodeList - ssr node list
* @param options - hippy context
*/
function convertToHippyNodeList(nodeList, options) {
return nodeList.map((item) => {
var _a;
// add props for every node
const props = convertSpecialKebabCaseToCamelCase(getNodeProps(item, nodeList, (_a = options === null || options === void 0 ? void 0 : options.context) === null || _a === void 0 ? void 0 : _a.isIOS));
return {
...item,
props,
};
});
}
/**
* flat hippy node tree to list structure
*
* @param parentNode - parent node
* @param nodeList - flatted node list
* @param pId - parent id
* @param index - the index position of the current node among sibling nodes
*/
function treeToList(parentNode, nodeList = [], pId, index) {
var _a, _b;
// parent node
nodeList.push({
id: parentNode.id,
pId: (_b = (_a = parentNode.pId) !== null && _a !== void 0 ? _a : pId) !== null && _b !== void 0 ? _b : 0,
index: index !== null && index !== void 0 ? index : 0,
name: parentNode.name,
props: parentNode.props,
tagName: parentNode.tagName,
});
// child node
const { children } = parentNode;
children === null || children === void 0 ? void 0 : children.forEach((v, i) => {
// filter native do not display node
let insertIndex = children.filter(c => c.name !== 'comment').indexOf(v);
// record the comment node index as the serial number of the current
// insertable child node, which is convenient for rebuilding the tree structure
if (v.name === 'comment') {
insertIndex = children
.slice(0, i)
.filter(c => c.name !== 'comment').length;
}
// handle all list
treeToList(v, nodeList, parentNode.id, insertIndex);
});
return nodeList;
}
/**
* create ssr root node
*
* @param rootContainer - id of root node
*/
function createSSRRootNode(rootContainer) {
return {
id: 1,
pId: 0,
index: 0,
name: 'View',
props: {
style: { flex: 1, display: 'flex' },
attributes: { id: rootContainer, class: '' },
},
tagName: 'div',
children: [],
};
}
/**
* key of ssr unique id
*/
const SSR_UNIQUE_ID_KEY = 'ssrUniqueIdKey';
/**
* generate unique id base on currentId. if no currentId provide. 1 will be the default value
*
* @param currentId - current used unique Id
*/
function generateUniqueId(currentId) {
let ssrUniqueId = currentId;
if (!ssrUniqueId)
ssrUniqueId = 1;
ssrUniqueId += 1;
// ids divisible by 10 are used by the native
if (ssrUniqueId % 10 === 0) {
ssrUniqueId += 1;
}
return ssrUniqueId;
}
/**
* generate hippy unique id at ssr. this is ssr helper
*
* @public
*/
function ssrGetUniqueId() {
var _a;
const currentInstance = getCurrentInstance();
const uniqueIdContext = (_a = currentInstance === null || currentInstance === void 0 ? void 0 : currentInstance.appContext) === null || _a === void 0 ? void 0 : _a.provides[SSR_UNIQUE_ID_KEY];
// generate unique id, and save in global context
// unique id generated by current unique id
uniqueIdContext.ssrUniqueId = generateUniqueId(uniqueIdContext === null || uniqueIdContext === void 0 ? void 0 : uniqueIdContext.ssrUniqueId);
return uniqueIdContext.ssrUniqueId;
}
/**
* get unique id at current vue app instance
*
* @param app - vue app instance
*
* @public
*/
function getCurrentUniqueId(app) {
return app._context.provides[SSR_UNIQUE_ID_KEY].ssrUniqueId;
}
/**
* convert hippy node list json string to hippy native node tree
*
* @param app - vue ssr app
* @param options - ssr options
*
* @public
*/
async function renderToHippyList(app, options) {
const startUniqueId = 1;
const uniqueIdContext = { ssrUniqueId: startUniqueId };
// An additional context needs to be provided here because ssrContext is mounted after
// the render function is called, but ssrGetUniqueId will be called in the render function
app.provide(SSR_UNIQUE_ID_KEY, uniqueIdContext);
const { rootContainer } = options;
// first, create root node with rootContainer.
// In the non-hydration mode of the client, the rootContainer node is created by the client
const rootNode = createSSRRootNode(rootContainer);
// second, we get hippy native node list string generated by ssr
const appContent = await renderToString(app, options);
// third, make ssr node list as children of rootContainer
const ssrNodeTree = getObjectNodeList([appContent], rootNode);
// render failure, return null
if (!ssrNodeTree) {
return null;
}
// flat node tree to array list
const nodeList = treeToList(ssrNodeTree);
// comment list append at the end
const commentList = nodeList.filter(v => v.name === 'comment');
// last, convert ssr node list to hippy node list, we add node props and return with comment list
return convertToHippyNodeList(nodeList.filter(v => v.name !== 'comment'), options).concat(commentList);
}
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* get hippy ssr style object
*
* @param raw - raw style
*
* @public
*/
function ssrRenderStyle(raw) {
var _a;
if (!raw) {
return '{}';
}
if (isString(raw)) {
// hippy doesn't support string style
return '{}';
}
return JSON.stringify((_a = normalizeStyle(raw)) !== null && _a !== void 0 ? _a : {});
}
/**
* get hippy ssr directive props
*
* @public
*/
function ssrGetDirectiveProps() {
return {};
}
/**
* render ssr slot.
* because template-compiled slots are always rendered as fragments, the vue/server-renderer wrap
* fragment content in html comment. like \<!--[--\>\{content\}\<!--]--\>. but hippy didn't recognize.
* so we use hippy comment node to replace it.
*
* @param slots - slot list
* @param slotName - slot name
* @param slotProps - slot props
* @param fallbackRenderFn - fallback render function
* @param push - push function
* @param parentComponent - parent component
* @param slotScopeId - slot scope id
*
* @public
*/
function ssrRenderSlot(slots, slotName, slotProps, fallbackRenderFn, push, parentComponent, slotScopeId) {
// template-compiled slots are always rendered as fragments
push('{"id": -1,"name":"comment","props":{"text":"["}},');
ssrRenderSlotInner(slots, slotName, slotProps, fallbackRenderFn, push, parentComponent, slotScopeId);
push('{"id": -1,"name":"comment","props":{"text":"]"}},');
}
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
/**
* get hippy native view's name, map with html tag
*
* @param tag - html tag or Vue component name
*/
function getHippyNativeViewName(tag) {
// Hippy built-in native view
const NATIVE_COMPONENT_MAP = {
View: 'View',
Image: 'Image',
ListView: 'ListView',
ListViewItem: 'ListViewItem',
Text: 'Text',
TextInput: 'TextInput',
WebView: 'WebView',
VideoPlayer: 'VideoPlayer',
// native inner view, just name different with View
ScrollView: 'ScrollView',
Swiper: 'ViewPager',
SwiperSlide: 'ViewPagerItem',
PullHeaderView: 'PullHeaderView',
PullFooterView: 'PullFooterView',
Dialog: 'Modal',
UlRefreshWrapper: 'RefreshWrapper',
UlRefresh: 'RefreshWrapperItemView',
Waterfall: 'WaterfallView',
WaterfallItem: 'WaterfallItem',
};
switch (tag) {
case 'div':
case 'button':
case 'form':
return NATIVE_COMPONENT_MAP.View;
case 'img':
return NATIVE_COMPONENT_MAP.Image;
case 'ul':
return NATIVE_COMPONENT_MAP.ListView;
case 'li':
return NATIVE_COMPONENT_MAP.ListViewItem;
case 'span':
case 'label':
case 'p':
case 'a':
return NATIVE_COMPONENT_MAP.Text;
case 'textarea':
case 'input':
return NATIVE_COMPONENT_MAP.TextInput;
case 'iframe':
return NATIVE_COMPONENT_MAP.WebView;
case 'hi-swiper':
return NATIVE_COMPONENT_MAP.Swiper;
case 'hi-swiper-slide':
return NATIVE_COMPONENT_MAP.SwiperSlide;
case 'hi-pull-header':
return NATIVE_COMPONENT_MAP.PullHeaderView;
case 'hi-pull-footer':
return NATIVE_COMPONENT_MAP.PullFooterView;
case 'dialog':
return NATIVE_COMPONENT_MAP.Dialog;
case 'hi-ul-refresh-wrapper':
return NATIVE_COMPONENT_MAP.UlRefreshWrapper;
case 'hi-refresh-wrapper-item':
return NATIVE_COMPONENT_MAP.UlRefresh;
case 'hi-waterfall':
return NATIVE_COMPONENT_MAP.Waterfall;
case 'hi-waterfall-item':
return NATIVE_COMPONENT_MAP.WaterfallItem;
default:
return tag;
}
}
/**
* parse vue tag/component name to real tag name
*
* @param compName
*/
function getHippyTagName(compName) {
// do not have need parse tag name now
return compName;
}
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { createComponentInstance, setCurrentRenderingInstance, setupComponent, renderComponentRoot, normalizeVNode, } = ssrUtils;
const commentNodeStr = '{"id": -1,"name":"comment","props":{"text":""}},';
/**
* return tag is text or not
*
* @param tag - tag name
*/
function isTextTag(tag) {
return ['span', 'p', 'label', 'a'].includes(tag);
}
function createBuffer() {
let appendable = false;
const buffer = [];
return {
getBuffer() {
// Return static buffer and await on items during unroll stage
return buffer;
},
push(item) {
const isStringItem = isString(item);
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item;
}
else {
buffer.push(item);
}
appendable = isStringItem;
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
// promise, or child buffer with async, mark as async.
// this allows skipping unnecessary await ticks during unroll stage
buffer.hasAsync = true;
}
},
};
}
function renderComponentVNode(vnode, parentComponent = null, slotScopeId) {
const instance = createComponentInstance(vnode, parentComponent, null);
const res = setupComponent(instance, true /* isSSR */);
const hasAsyncSetup = isPromise(res);
const prefetches = instance.sp; /* LifecycleHooks.SERVER_PREFETCH */
if (hasAsyncSetup || prefetches) {
let p = hasAsyncSetup
? res
: Promise.resolve();
if (prefetches) {
p = p
.then(async () => Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy))))
// Note: error display is already done by the wrapped lifecycle hook function.
.catch(() => { });
}
return p.then(() => renderComponentSubTree(instance, slotScopeId));
}
return renderComponentSubTree(instance, slotScopeId);
}
function renderComponentSubTree(rawInstance, slotScopeId) {
const comp = rawInstance.type;
const { getBuffer, push } = createBuffer();
if (isFunction(comp)) {
// this is functional component
const root = renderComponentRoot(rawInstance);
// #5817 scope ID attrs not falling through if functional component doesn't
// have props
if (!comp.props) {
// eslint-disable-next-line no-restricted-syntax
for (const key in rawInstance.attrs) {
if (key.startsWith('data-v-')) {
(root.props || (root.props = {}))[key] = '';
}
}
}
renderVNode(push, (rawInstance.subTree = root), rawInstance, slotScopeId);
}
else {
// pay attention please, our app do not support runtime compile, so we doesn't need ssrCompile logic
// if (
// (!instance.render || instance.render === NOOP)
// && !instance.ssrRender
// && !comp.ssrRender
// && isString(comp.template)
// ) {
// comp.ssrRender = ssrCompile(comp.template, instance);
// }
// because some props of ComponentInternalInstance, like ssrRender,scope,setupState, and so on. was set to
// internal type, so we used here as any to avoid ts type error
const instance = rawInstance;
// perf: enable caching of computed getters during render
// since there cannot be state mutations during render.
for (const e of instance.scope.effects) {
if (e.computed)
e.computed._cacheable = true;
}
const ssrRender = instance.ssrRender || comp.ssrRender;
if (ssrRender) {
// optimized
// resolve fallthrough attrs
let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined;
let hasCloned = false;
let cur = instance;
while (true) {
const { scopeId } = cur.vnode;
if (scopeId) {
if (!hasCloned) {
attrs = { ...attrs };
hasCloned = true;
}
attrs[scopeId] = '';
}
const { parent } = cur;
if ((parent === null || parent === void 0 ? void 0 : parent.subTree) === cur.vnode) {
// parent is a non-SSR compiled component and is rendering this
// component as root. inherit its scopeId if present.
cur = parent;
}
else {
break;
}
}
if (slotScopeId) {
if (!hasCloned)
attrs = { ...attrs };
attrs[slotScopeId.trim()] = '';
}
// set current rendering instance for asset resolution
const prev = setCurrentRenderingInstance(instance);
try {
ssrRender(instance.proxy, push, instance, attrs,
// compiler-optimized bindings
instance.props, instance.setupState, instance.data, instance.ctx);
}
finally {
setCurrentRenderingInstance(prev);
}
}
else if (instance.render && instance.render !== NOOP) {
renderVNode(push, (instance.subTree = renderComponentRoot(instance)), instance, slotScopeId);
}
else {
const componentName = comp.name || comp.__file || '<Anonymous>';
warn(`Component ${componentName} is missing template or render function.`);
push(commentNodeStr);
}
}
return getBuffer();
}
/**
* render vNode
*
* @param push - push function
* @param vnode - vnode
* @param parentComponent - parent component
* @param slotScopeId - slot scoped id
*
* @public
*/
function renderVNode(push, vnode, parentComponent, slotScopeId) {
const { type, shapeFlag, children } = vnode;
switch (type) {
case Text:
// vue text means <div>content</div> content. hippy do not support it. should use
// <div><span>content</span></div>, all text content should wrap in span/p/label.
push(`{"id": ${ssrGetUniqueId()},"name":"Text","props":{"text":"${children}"}},`);
break;
case Static:
// hippy do not support static hoist
break;
case Comment:
push(children
? `{"id": -1,"name":"comment","props":{"text":"${children}"}},`
: commentNodeStr);
break;
case Fragment:
if (vnode === null || vnode === void 0 ? void 0 : vnode.slotScopeIds) {
slotScopeId = (slotScopeId ? `${slotScopeId} ` : '') + vnode.slotScopeIds.join(' ');
}
// fixme: fragment do not totally test, should fixme later
push('{"id": -1,"name":"comment","props":{"text":"["}},'); // open
renderVNodeChildren(push, children, parentComponent, slotScopeId, true);
push('{"id": -1,"name":"comment","props":{"text":"]"}},'); // close
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
renderElementVNode(push, vnode, parentComponent, slotScopeId);
}
else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent, slotScopeId));
}
else if (shapeFlag & ShapeFlags.TELEPORT) {
// hippy do not support teleport
renderTeleportVNode(push, vnode, parentComponent, slotScopeId);
}
else if (shapeFlag & ShapeFlags.SUSPENSE) ;
else {
warn('[@vue/server-renderer] Invalid VNode type:', type, `(${typeof type})`);
}
}
}
function renderVNodeChildren(push, children, parentComponent, slotScopeId, asFragment = false) {
if (!asFragment) {
// hippy fragment no need to insert children part
push('"children":[');
}
for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId);
}
if (!asFragment) {
// hippy fragment no need to insert children part
push('],');
}
}
function renderElementVNode(push, vnode, parentComponent, slotScopeId) {
var _a, _b, _c;
const tag = vnode.type;
const { children, shapeFlag, dirs } = vnode;
let openTag = `{"id":${ssrGetUniqueId()},"index":0,"name":"${getHippyNativeViewName(tag)}","tagName":"${getHippyTagName(tag)}","props":`;
let props = (_a = vnode.props) !== null && _a !== void 0 ? _a : {};
if (dirs) {
props = applySSRDirectives(vnode, props, dirs);
}
// because native tag compiled as custom element, so the native tag doesn't have
// scopeId(scopeId generated at compiler time)
// we use the child scopeId or parent scopeId if exist
let { scopeId } = vnode;
if (!scopeId && (children === null || children === void 0 ? void 0 : children.length)) {
scopeId = (_b = children[0].scopeId) !== null && _b !== void 0 ? _b : null;
}
// use parent scopedId if exist
if (!scopeId && (parentComponent === null || parentComponent === void 0 ? void 0 : parentComponent.vnode)) {
scopeId = (_c = parentComponent.vnode.scopeId) !== null && _c !== void 0 ? _c : null;
}
if (scopeId && typeof props[scopeId] === 'undefined') {
// custom element do not generate scopeId, so inserted here
props[scopeId] = '';
vnode.scopeId = scopeId;
}
// span/label/p/a, these nodes are all text node in native. so we should set text prop
if (isTextTag(tag)) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// text node children is array
if (children === null || children === void 0 ? void 0 : children.length) {
const textChild = children.filter((item) => {
if (typeof item === 'object') {
// only VNode & VNode Children is Object, other is base type
const text = item;
return (text === null || text === void 0 ? void 0 : text.shapeFlag) & ShapeFlags.TEXT_CHILDREN;
}
return false;
});
if (textChild.length) {
const child = textChild[0];
props.text = child === null || child === void 0 ? void 0 : child.children;
// if text child node has scopedId attr, need insert to props
if (child === null || child === void 0 ? void 0 : child.scopeId) {
props[child.scopeId] = '';
}
}
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN && children) {
// text node children is string
props.text = children;
}
}
openTag += `${JSON.stringify(props)},`;
push(`${openTag}`);
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
renderVNodeChildren(push, children, parentComponent, slotScopeId);
}
push('},');
}
function applySSRDirectives(vnode, rawProps, dirs) {
const toMerge = [];
for (let i = 0; i < dirs.length; i++) {
const binding = dirs[i];
const { dir: { getSSRProps }, } = binding;
if (getSSRProps) {
const props = getSSRProps(binding, vnode);
if (props)
toMerge.push(props);
}
}
return mergeProps(rawProps !== null && rawProps !== void 0 ? rawProps : {}, ...toMerge);
}
function renderTeleportVNode(push, vnode, parentComponent, slotScopeId) {
var _a, _b;
const target = (_a = vnode.props) === null || _a === void 0 ? void 0 : _a.to;
const disabled = (_b = vnode.props) === null || _b === void 0 ? void 0 : _b.disabled;
if (!target) {
if (!disabled) {
warn('[@vue/server-renderer] Teleport is missing target prop.');
}
}
if (!isString(target)) {
warn('[@vue/server-renderer] Teleport target must be a query selector string.');
}
ssrRenderTeleport(push, (_push) => {
renderVNodeChildren(_push, vnode.children, parentComponent, slotScopeId);
}, target, disabled || disabled === '', parentComponent);
}
function ssrRenderTeleport(parentPush, contentRenderFn, target, disabled, parentComponent) {
var _a;
parentPush('{"id": -1,"name":"comment","props":{"text":"teleport start"}},');
const context = parentComponent.appContext.provides[ssrContextKey];
const teleportBuffers = (_a = context.__teleportBuffers) !== null && _a !== void 0 ? _a : (context.__teleportBuffers = {});
const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = []);
// record current index of the target buffer to handle nested teleports
// since the parent needs to be rendered before the child
const bufferIndex = targetBuffer.length;
let teleportContent;
if (disabled) {
contentRenderFn(parentPush);
teleportContent = '{"id": -1,"name":"comment","props":{"text":"teleport anchor"}},';
}
else {
const { getBuffer, push } = createBuffer();
contentRenderFn(push);
push('{"id": -1,"name":"comment","props":{"text":"teleport anchor"}},');
teleportContent = getBuffer();
}
targetBuffer.splice(bufferIndex, 0, teleportContent);
parentPush('{"id": -1,"name":"comment","props":{"text":"teleport end"}},');
}
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* render component for server side
*
* @param comp - vue component
* @param props - component props
* @param children - component children
* @param parentComponent - parent
* @param slotScopeId - slot scoped id
*
* @public
*/
function ssrRenderComponent(comp, props = null, children = null, parentComponent = null, slotScopeId) {
return renderComponentVNode(createVNode(comp, props, children), parentComponent, slotScopeId);
}
export { getCurrentUniqueId, renderToHippyList, ssrGetDirectiveProps, ssrGetUniqueId, ssrRenderComponent, ssrRenderSlot, ssrRenderStyle, renderVNode as ssrRenderVNode };