// 2. create props proxy
// the propsProxy is a reactive AND readonly proxy to the actual props.
// it will be updated in resolveProps() on updates before render
- const propsProxy = (instance.propsProxy = isInSSRComponentSetup
+ const propsProxy = (instance.propsProxy = isSSR
? instance.props
: shallowReadonly(instance.props))
// 3. call setup()
currentSuspense = null
if (isPromise(setupResult)) {
- if (isInSSRComponentSetup) {
+ if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult.then(resolvedResult => {
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
import { renderToString } from '../src/renderToString'
describe('SSR Suspense', () => {
+ let logError: jest.SpyInstance
+
+ beforeEach(() => {
+ logError = jest.spyOn(console, 'error').mockImplementation(() => {})
+ })
+
+ afterEach(() => {
+ logError.mockRestore()
+ })
+
const ResolvingAsync = {
async setup() {
return () => h('div', 'async')
const RejectingAsync = {
setup() {
- return new Promise((_, reject) => reject())
+ return new Promise((_, reject) => reject('foo'))
}
}
}
expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
+ expect(logError).not.toHaveBeenCalled()
})
test('fallback', async () => {
}
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+ expect(logError).toHaveBeenCalled()
})
test('2 components', async () => {
expect(await renderToString(createApp(Comp))).toBe(
`<div><div>async</div><div>async</div></div>`
)
+ expect(logError).not.toHaveBeenCalled()
})
test('resolving component + rejecting component', async () => {
}
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+ expect(logError).toHaveBeenCalled()
})
test('failing suspense in passing suspense', async () => {
expect(await renderToString(createApp(Comp))).toBe(
`<div><div>async</div><div>fallback 2</div></div>`
)
+ expect(logError).toHaveBeenCalled()
})
test('passing suspense in failing suspense', async () => {
}
expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
+ expect(logError).toHaveBeenCalled()
})
})
Fragment,
ssrUtils,
Slots,
- warn,
createApp,
ssrContextKey
} from 'vue'
)
}
+export const AsyncSetupErrorMarker = Symbol('Vue async setup error')
+
function renderComponentVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
true /* isSSR */
)
if (isPromise(res)) {
- return res.then(() => renderComponentSubTree(instance))
+ return res
+ .catch(err => {
+ // normalize async setup rejection
+ if (!(err instanceof Error)) {
+ err = new Error(String(err))
+ }
+ err[AsyncSetupErrorMarker] = true
+ console.error(
+ `[@vue/server-renderer]: Uncaught error in async setup:\n`,
+ err
+ )
+ // rethrow for suspense
+ throw err
+ })
+ .then(() => renderComponentSubTree(instance))
} else {
return renderComponentSubTree(instance)
}
isNativeTag: instance.appContext.config.isNativeTag || NO,
onError(err: CompilerError) {
if (__DEV__) {
- const message = `Template compilation error: ${err.message}`
+ const message = `[@vue/server-renderer] Template compilation error: ${
+ err.message
+ }`
const codeFrame =
err.loc &&
generateCodeFrame(
err.loc.start.offset,
err.loc.end.offset
)
- warn(codeFrame ? `${message}\n${codeFrame}` : message)
+ console.error(codeFrame ? `${message}\n${codeFrame}` : message)
} else {
throw err
}
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
- renderElement(push, vnode, parentComponent)
+ renderElementVNode(push, vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent))
} else if (shapeFlag & ShapeFlags.PORTAL) {
- renderPortal(vnode, parentComponent)
+ renderPortalVNode(vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
- push(renderSuspense(vnode, parentComponent))
+ push(renderSuspenseVNode(vnode, parentComponent))
} else {
- console.warn(
+ console.error(
'[@vue/server-renderer] Invalid VNode type:',
type,
`(${typeof type})`
}
}
-function renderElement(
+function renderElementVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance
}
}
-function renderPortal(
+function renderPortalVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance
) {
const target = vnode.props && vnode.props.target
if (!target) {
- console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
+ console.error(`[@vue/server-renderer] Portal is missing target prop.`)
return []
}
if (!isString(target)) {
- console.warn(
+ console.error(
`[@vue/server-renderer] Portal target must be a query selector string.`
)
return []
}
}
-async function renderSuspense(
+async function renderSuspenseVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance
): Promise<ResolvedSSRBuffer> {
try {
const { push, getBuffer } = createBuffer()
renderVNode(push, content, parentComponent)
+ // await here so error can be caught
return await getBuffer()
- } catch {
- const { push, getBuffer } = createBuffer()
- renderVNode(push, fallback, parentComponent)
- return getBuffer()
+ } catch (e) {
+ if (e[AsyncSetupErrorMarker]) {
+ const { push, getBuffer } = createBuffer()
+ renderVNode(push, fallback, parentComponent)
+ return getBuffer()
+ } else {
+ throw e
+ }
}
}