// For server-renderer
export { createComponentInstance, setupComponent } from './component'
+export { renderComponentRoot } from './componentRenderUtils'
// Types -----------------------------------------------------------------------
isString,
isObject,
EMPTY_ARR,
- extend
+ extend,
+ normalizeClass,
+ normalizeStyle
} from '@vue/shared'
import {
ComponentInternalInstance,
vnode.shapeFlag |= type
}
-function normalizeStyle(
- value: unknown
-): Record<string, string | number> | void {
- if (isArray(value)) {
- const res: Record<string, string | number> = {}
- for (let i = 0; i < value.length; i++) {
- const normalized = normalizeStyle(value[i])
- if (normalized) {
- for (const key in normalized) {
- res[key] = normalized[key]
- }
- }
- }
- return res
- } else if (isObject(value)) {
- return value
- }
-}
-
-export function normalizeClass(value: unknown): string {
- let res = ''
- if (isString(value)) {
- res = value
- } else if (isArray(value)) {
- for (let i = 0; i < value.length; i++) {
- res += normalizeClass(value[i]) + ' '
- }
- } else if (isObject(value)) {
- for (const name in value) {
- if (value[name]) {
- res += name + ' '
- }
- }
- }
- return res.trim()
-}
-
const handlersRE = /^on|^vnode/
export function mergeProps(...args: (Data & VNodeProps)[]) {
--- /dev/null
+test('ssr: escape HTML', () => {})
--- /dev/null
+// import { renderToString, renderComponent } from '../src'
+
+describe('ssr: renderToString', () => {
+ test('basic', () => {})
+
+ test('nested components', () => {})
+
+ test('nested components with optimized slots', () => {})
+
+ test('mixing optimized / vnode components', () => {})
+
+ test('nested components with vnode slots', () => {})
+
+ test('async components', () => {})
+
+ test('parallel async components', () => {})
+})
--- /dev/null
+describe('ssr: render raw vnodes', () => {
+ test('class', () => {})
+
+ test('styles', () => {
+ // only render numbers for properties that allow no unit numbers
+ })
+
+ describe('attrs', () => {
+ test('basic', () => {})
+
+ test('boolean attrs', () => {})
+
+ test('enumerated attrs', () => {})
+
+ test('skip falsy values', () => {})
+ })
+
+ describe('domProps', () => {
+ test('innerHTML', () => {})
+
+ test('textContent', () => {})
+
+ test('textarea', () => {})
+
+ test('other renderable domProps', () => {
+ // also test camel to kebab case conversion for some props
+ })
+ })
+})
-import { toDisplayString } from '@vue/shared'
-
const escapeRE = /["'&<>]/
export function escape(string: unknown) {
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}
-
-export function interpolate(value: unknown) {
- return escape(toDisplayString(value))
-}
-import {
- App,
- Component,
- ComponentInternalInstance,
- createComponentInstance,
- setupComponent,
- VNode,
- createVNode
-} from 'vue'
-import { isString, isPromise, isArray } from '@vue/shared'
+import { toDisplayString } from 'vue'
-export * from './helpers'
+export { renderToString, renderComponent } from './renderToString'
-type SSRBuffer = SSRBufferItem[]
-type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
-type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+export {
+ renderVNode,
+ renderClass,
+ renderStyle,
+ renderProps
+} from './renderVnode'
-function createBuffer() {
- let appendable = false
- let hasAsync = false
- const buffer: SSRBuffer = []
- return {
- buffer,
- hasAsync() {
- return hasAsync
- },
- push(item: SSRBufferItem) {
- const isStringItem = isString(item)
- if (appendable && isStringItem) {
- buffer[buffer.length - 1] += item as string
- } else {
- buffer.push(item)
- }
- appendable = isStringItem
- if (!isStringItem && !isArray(item)) {
- // promise
- hasAsync = true
- }
- }
- }
-}
-
-function unrollBuffer(buffer: ResolvedSSRBuffer): string {
- let ret = ''
- for (let i = 0; i < buffer.length; i++) {
- const item = buffer[i]
- if (isString(item)) {
- ret += item
- } else {
- ret += unrollBuffer(item)
- }
- }
- return ret
-}
-
-export async function renderToString(app: App): Promise<string> {
- const resolvedBuffer = (await renderComponent(
- app._component,
- app._props
- )) as ResolvedSSRBuffer
- return unrollBuffer(resolvedBuffer)
-}
-
-export function renderComponent(
- comp: Component,
- props: Record<string, any> | null = null,
- children: VNode['children'] = null,
- parentComponent: ComponentInternalInstance | null = null
-): ResolvedSSRBuffer | Promise<SSRBuffer> {
- const vnode = createVNode(comp, props, children)
- const instance = createComponentInstance(vnode, parentComponent)
- const res = setupComponent(instance, null)
- if (isPromise(res)) {
- return res.then(() => innerRenderComponent(comp, instance))
- } else {
- return innerRenderComponent(comp, instance)
- }
-}
+export { escape } from './escape'
-function innerRenderComponent(
- comp: Component,
- instance: ComponentInternalInstance
-): ResolvedSSRBuffer | Promise<SSRBuffer> {
- const { buffer, push, hasAsync } = createBuffer()
- if (typeof comp === 'function') {
- // TODO FunctionalComponent
- } else {
- if (comp.ssrRender) {
- // optimized
- comp.ssrRender(push, instance.proxy)
- } else if (comp.render) {
- // TODO fallback to vdom serialization
- } else {
- // TODO warn component missing render function
- }
- }
- // If the current component's buffer contains any Promise from async children,
- // then it must return a Promise too. Otherwise this is a component that
- // contains only sync children so we can avoid the async book-keeping overhead.
- return hasAsync()
- ? // TS can't figure out the typing due to recursive appearance of Promise
- Promise.all(buffer as any)
- : (buffer as ResolvedSSRBuffer)
+export function interpolate(value: unknown) {
+ return escape(toDisplayString(value))
}
--- /dev/null
+import {
+ App,
+ Component,
+ ComponentInternalInstance,
+ VNode,
+ createComponentInstance,
+ setupComponent,
+ createVNode,
+ renderComponentRoot
+} from 'vue'
+import { isString, isPromise, isArray, isFunction } from '@vue/shared'
+import { renderVNode } from './renderVnode'
+
+export type SSRBuffer = SSRBufferItem[]
+export type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
+export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+
+function createBuffer() {
+ let appendable = false
+ let hasAsync = false
+ const buffer: SSRBuffer = []
+ return {
+ buffer,
+ hasAsync() {
+ return hasAsync
+ },
+ push(item: SSRBufferItem) {
+ const isStringItem = isString(item)
+ if (appendable && isStringItem) {
+ buffer[buffer.length - 1] += item as string
+ } else {
+ buffer.push(item)
+ }
+ appendable = isStringItem
+ if (!isStringItem && !isArray(item)) {
+ // promise
+ hasAsync = true
+ }
+ }
+ }
+}
+
+function unrollBuffer(buffer: ResolvedSSRBuffer): string {
+ let ret = ''
+ for (let i = 0; i < buffer.length; i++) {
+ const item = buffer[i]
+ if (isString(item)) {
+ ret += item
+ } else {
+ ret += unrollBuffer(item)
+ }
+ }
+ return ret
+}
+
+export async function renderToString(app: App): Promise<string> {
+ const resolvedBuffer = (await renderComponent(
+ app._component,
+ app._props
+ )) as ResolvedSSRBuffer
+ return unrollBuffer(resolvedBuffer)
+}
+
+export function renderComponent(
+ comp: Component,
+ props: Record<string, any> | null = null,
+ children: VNode['children'] = null,
+ parentComponent: ComponentInternalInstance | null = null
+): ResolvedSSRBuffer | Promise<SSRBuffer> {
+ const vnode = createVNode(comp, props, children)
+ const instance = createComponentInstance(vnode, parentComponent)
+ const res = setupComponent(instance, null)
+ if (isPromise(res)) {
+ return res.then(() => innerRenderComponent(comp, instance))
+ } else {
+ return innerRenderComponent(comp, instance)
+ }
+}
+
+function innerRenderComponent(
+ comp: Component,
+ instance: ComponentInternalInstance
+): ResolvedSSRBuffer | Promise<SSRBuffer> {
+ const { buffer, push, hasAsync } = createBuffer()
+ if (isFunction(comp)) {
+ renderVNode(push, renderComponentRoot(instance))
+ } else {
+ if (comp.ssrRender) {
+ // optimized
+ comp.ssrRender(push, instance.proxy)
+ } else if (comp.render) {
+ renderVNode(push, renderComponentRoot(instance))
+ } else {
+ // TODO on the fly template compilation support
+ throw new Error(
+ `Component ${
+ comp.name ? `${comp.name} ` : ``
+ } is missing render function.`
+ )
+ }
+ }
+ // If the current component's buffer contains any Promise from async children,
+ // then it must return a Promise too. Otherwise this is a component that
+ // contains only sync children so we can avoid the async book-keeping overhead.
+ return hasAsync()
+ ? // TS can't figure out the typing due to recursive appearance of Promise
+ Promise.all(buffer as any)
+ : (buffer as ResolvedSSRBuffer)
+}
--- /dev/null
+import { VNode } from 'vue'
+import { SSRBufferItem } from './renderToString'
+
+export function renderVNode(
+ push: (item: SSRBufferItem) => void,
+ vnode: VNode
+) {}
+
+export function renderProps() {}
+
+export function renderClass() {}
+
+export function renderStyle() {}
export * from './codeframe'
export * from './domTagConfig'
export * from './mockWarn'
+export * from './normalizeProp'
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})
--- /dev/null
+import { isArray, isString, isObject } from './'
+
+export function normalizeStyle(
+ value: unknown
+): Record<string, string | number> | void {
+ if (isArray(value)) {
+ const res: Record<string, string | number> = {}
+ for (let i = 0; i < value.length; i++) {
+ const normalized = normalizeStyle(value[i])
+ if (normalized) {
+ for (const key in normalized) {
+ res[key] = normalized[key]
+ }
+ }
+ }
+ return res
+ } else if (isObject(value)) {
+ return value
+ }
+}
+
+export function normalizeClass(value: unknown): string {
+ let res = ''
+ if (isString(value)) {
+ res = value
+ } else if (isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ res += normalizeClass(value[i]) + ' '
+ }
+ } else if (isObject(value)) {
+ for (const name in value) {
+ if (value[name]) {
+ res += name + ' '
+ }
+ }
+ }
+ return res.trim()
+}