UNPKG

@hippy/vue-next-server-renderer

Version:

Vue-Next server renderer for Hippy native framework

971 lines (962 loc) 37.2 kB
/*! * @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 };