// @vitest-environment node import Vue from 'vue' import { compileWithWebpack, createWebpackBundleRenderer } from './compile-with-webpack' import { createRenderer } from 'server/index' import VueSSRClientPlugin from 'server/webpack-plugin/client' import { RenderOptions } from 'server/create-renderer' const defaultTemplate = `` const interpolateTemplate = `{{ title }}{{{ snippet }}}` async function generateClientManifest(file: string) { const fs = await compileWithWebpack(file, { output: { path: '/', publicPath: '/', filename: '[name].js' }, optimization: { runtimeChunk: { name: 'manifest' } }, plugins: [new VueSSRClientPlugin()] }) return JSON.parse(fs.readFileSync('/vue-ssr-client-manifest.json', 'utf-8')) } async function createRendererWithManifest( file: string, options?: RenderOptions ) { const clientManifest = await generateClientManifest(file) return createWebpackBundleRenderer( file, Object.assign( { asBundle: true, template: defaultTemplate, clientManifest }, options ) ) } describe('SSR: template option', () => { it('renderToString', async () => { const renderer = createRenderer({ template: defaultTemplate }) const context = { head: '', styles: '', state: { a: 1 } } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `${context.head}${context.styles}` + `
hi
` + `` + `` ) }) it('renderToString with interpolation', async () => { const renderer = createRenderer({ template: interpolateTemplate }) const context = { title: '', snippet: '
foo
', head: '', styles: '', state: { a: 1 } } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `` + // double mustache should be escaped `<script>hacks</script>` + `${context.head}${context.styles}` + `
hi
` + `` + // triple should be raw `
foo
` + `` ) }) it('renderToString with interpolation and context.rendered', async () => { const renderer = createRenderer({ template: interpolateTemplate }) const context = { title: '', snippet: '
foo
', head: '', styles: '', state: { a: 0 }, rendered: context => { context.state.a = 1 } } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `` + // double mustache should be escaped `<script>hacks</script>` + `${context.head}${context.styles}` + `
hi
` + `` + // triple should be raw `
foo
` + `` ) }) it('renderToString w/ template function', async () => { const renderer = createRenderer({ template: (content, context) => `${context.head}${content}` }) const context = { head: '' } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `${context.head}
hi
` ) }) it('renderToString w/ template function returning Promise', async () => { const renderer = createRenderer({ template: (content, context) => new Promise(resolve => { setTimeout(() => { resolve(`${context.head}${content}`) }, 0) }) }) const context = { head: '' } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `${context.head}
hi
` ) }) it('renderToString w/ template function returning Promise w/ rejection', async () => { const renderer = createRenderer({ template: () => new Promise((resolve, reject) => { setTimeout(() => { reject(new Error(`foo`)) }, 0) }) }) const context = { head: '' } try { await renderer.renderToString( new Vue({ template: '
hi
' }), context ) } catch (err: any) { expect(err.message).toBe(`foo`) } }) it('renderToStream', async () => { const renderer = createRenderer({ template: defaultTemplate }) const context = { head: '', styles: '', state: { a: 1 } } const res = await new Promise((resolve, reject) => { const stream = renderer.renderToStream( new Vue({ template: '
hi
' }), context ) let res = '' stream.on('data', chunk => { res += chunk }) stream.on('error', reject) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( `${context.head}${context.styles}` + `
hi
` + `` + `` ) }) it('renderToStream with interpolation', async () => { const renderer = createRenderer({ template: interpolateTemplate }) const context = { title: '', snippet: '
foo
', head: '', styles: '', state: { a: 1 } } const res = await new Promise((resolve, reject) => { const stream = renderer.renderToStream( new Vue({ template: '
hi
' }), context ) let res = '' stream.on('data', chunk => { res += chunk }) stream.on('error', reject) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( `` + // double mustache should be escaped `<script>hacks</script>` + `${context.head}${context.styles}` + `
hi
` + `` + // triple should be raw `
foo
` + `` ) }) it('renderToStream with interpolation and context.rendered', async () => { const renderer = createRenderer({ template: interpolateTemplate }) const context = { title: '', snippet: '
foo
', head: '', styles: '', state: { a: 0 }, rendered: context => { context.state.a = 1 } } const res = await new Promise((resolve, reject) => { const stream = renderer.renderToStream( new Vue({ template: '
hi
' }), context ) let res = '' stream.on('data', chunk => { res += chunk }) stream.on('error', reject) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( `` + // double mustache should be escaped `<script>hacks</script>` + `${context.head}${context.styles}` + `
hi
` + `` + // triple should be raw `
foo
` + `` ) }) it('bundleRenderer + renderToString', async () => { const renderer = await createWebpackBundleRenderer('app.js', { asBundle: true, template: defaultTemplate }) const context: any = { head: '', styles: '', state: { a: 1 }, url: '/test' } const res = await renderer.renderToString(context) expect(res).toContain( `${context.head}${context.styles}` + `
/test
` + `` + `` ) expect(context.msg).toBe('hello') }) it('bundleRenderer + renderToStream', async () => { const renderer = await createWebpackBundleRenderer('app.js', { asBundle: true, template: defaultTemplate }) const context: any = { head: '', styles: '', state: { a: 1 }, url: '/test' } const res = await new Promise(resolve => { const stream = renderer.renderToStream(context) let res = '' stream.on('data', chunk => { res += chunk.toString() }) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( `${context.head}${context.styles}` + `
/test
` + `` + `` ) expect(context.msg).toBe('hello') }) const expectedHTMLWithManifest = (options: any = {}) => `` + // used chunks should have preload `` + `` + `` + `` + // images and fonts are only preloaded when explicitly asked for (options.preloadOtherAssets ? `` : ``) + (options.preloadOtherAssets ? `` : ``) + // unused chunks should have prefetch (options.noPrefetch ? `` : ``) + // css assets should be loaded `` + `` + `
async test.woff2 test.png
` + // state should be inlined before scripts `` + // manifest chunk should be first `` + // async chunks should be before main chunk `` + `` + `` createClientManifestAssertions(true) createClientManifestAssertions(false) function createClientManifestAssertions(runInNewContext) { it('bundleRenderer + renderToString + clientManifest ()', async () => { const renderer = await createRendererWithManifest('split.js', { runInNewContext }) const res = await renderer.renderToString({ state: { a: 1 } }) expect(res).toContain(expectedHTMLWithManifest()) }) it('bundleRenderer + renderToStream + clientManifest + shouldPreload', async () => { const renderer = await createRendererWithManifest('split.js', { runInNewContext, shouldPreload: (file, type) => { if ( type === 'image' || type === 'script' || type === 'font' || type === 'style' ) { return true } } }) const res = await new Promise(resolve => { const stream = renderer.renderToStream({ state: { a: 1 } }) let res = '' stream.on('data', chunk => { res += chunk.toString() }) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( expectedHTMLWithManifest({ preloadOtherAssets: true }) ) }) it('bundleRenderer + renderToStream + clientManifest + shouldPrefetch', async () => { const renderer = await createRendererWithManifest('split.js', { runInNewContext, shouldPrefetch: (file, type) => { if (type === 'script') { return false } } }) const res = await new Promise(resolve => { const stream = renderer.renderToStream({ state: { a: 1 } }) let res = '' stream.on('data', chunk => { res += chunk.toString() }) stream.on('end', () => { resolve(res) }) }) expect(res).toContain( expectedHTMLWithManifest({ noPrefetch: true }) ) }) it('bundleRenderer + renderToString + clientManifest + inject: false', async () => { const renderer = await createRendererWithManifest('split.js', { runInNewContext, template: `` + `{{{ renderResourceHints() }}}{{{ renderStyles() }}}` + `{{{ renderState({ windowKey: '__FOO__', contextKey: 'foo' }) }}}{{{ renderScripts() }}}` + ``, inject: false }) const context = { foo: { a: 1 } } const res = await renderer.renderToString(context) expect(res).toContain( expectedHTMLWithManifest({ stateKey: '__FOO__' }) ) }) it('bundleRenderer + renderToString + clientManifest + no template', async () => { const renderer = await createRendererWithManifest('split.js', { runInNewContext, template: null as any }) const context: any = { foo: { a: 1 } } const res = await renderer.renderToString(context) const customOutput = `${ context.renderResourceHints() + context.renderStyles() }${ res + context.renderState({ windowKey: '__FOO__', contextKey: 'foo' }) + context.renderScripts() }` expect(customOutput).toContain( expectedHTMLWithManifest({ stateKey: '__FOO__' }) ) }) it('whitespace insensitive interpolation', async () => { const interpolateTemplate = `{{title}}{{{snippet}}}` const renderer = createRenderer({ template: interpolateTemplate }) const context = { title: '', snippet: '
foo
', head: '', styles: '', state: { a: 1 } } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `` + // double mustache should be escaped `<script>hacks</script>` + `${context.head}${context.styles}` + `
hi
` + `` + // triple should be raw `
foo
` + `` ) }) it('renderToString + nonce', async () => { const interpolateTemplate = `hello` const renderer = createRenderer({ template: interpolateTemplate }) const context = { state: { a: 1 }, nonce: '4AEemGb0xJptoIGFP3Nd' } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `` + `hello` + `` + `
hi
` + `` + `` ) }) it('renderToString + custom serializer', async () => { const expected = `{"foo":123}` const renderer = createRenderer({ template: defaultTemplate, serializer: () => expected }) const context = { state: { a: 1 } } const res = await renderer.renderToString( new Vue({ template: '
hi
' }), context ) expect(res).toContain( `` ) }) } })