1// @vitest-environment node23import Vue from 'vue'4import {5 compileWithWebpack,6 createWebpackBundleRenderer7} from './compile-with-webpack'8import { createRenderer } from 'server/index'9import VueSSRClientPlugin from 'server/webpack-plugin/client'10import { RenderOptions } from 'server/create-renderer'1112const defaultTemplate = `<html><head></head><body><!--vue-ssr-outlet--></body></html>`13const interpolateTemplate = `<html><head><title>{{ title }}</title></head><body><!--vue-ssr-outlet-->{{{ snippet }}}</body></html>`1415async function generateClientManifest(file: string) {16 const fs = await compileWithWebpack(file, {17 output: {18 path: '/',19 publicPath: '/',20 filename: '[name].js'21 },22 optimization: {23 runtimeChunk: {24 name: 'manifest'25 }26 },27 plugins: [new VueSSRClientPlugin()]28 })29 return JSON.parse(fs.readFileSync('/vue-ssr-client-manifest.json', 'utf-8'))30}3132async function createRendererWithManifest(33 file: string,34 options?: RenderOptions35) {36 const clientManifest = await generateClientManifest(file)37 return createWebpackBundleRenderer(38 file,39 Object.assign(40 {41 asBundle: true,42 template: defaultTemplate,43 clientManifest44 },45 options46 )47 )48}4950describe('SSR: template option', () => {51 it('renderToString', async () => {52 const renderer = createRenderer({53 template: defaultTemplate54 })5556 const context = {57 head: '<meta name="viewport" content="width=device-width">',58 styles: '<style>h1 { color: red }</style>',59 state: { a: 1 }60 }6162 const res = await renderer.renderToString(63 new Vue({64 template: '<div>hi</div>'65 }),66 context67 )6869 expect(res).toContain(70 `<html><head>${context.head}${context.styles}</head><body>` +71 `<div data-server-rendered="true">hi</div>` +72 `<script>window.__INITIAL_STATE__={"a":1}</script>` +73 `</body></html>`74 )75 })7677 it('renderToString with interpolation', async () => {78 const renderer = createRenderer({79 template: interpolateTemplate80 })8182 const context = {83 title: '<script>hacks</script>',84 snippet: '<div>foo</div>',85 head: '<meta name="viewport" content="width=device-width">',86 styles: '<style>h1 { color: red }</style>',87 state: { a: 1 }88 }8990 const res = await renderer.renderToString(91 new Vue({92 template: '<div>hi</div>'93 }),94 context95 )9697 expect(res).toContain(98 `<html><head>` +99 // double mustache should be escaped100 `<title><script>hacks</script></title>` +101 `${context.head}${context.styles}</head><body>` +102 `<div data-server-rendered="true">hi</div>` +103 `<script>window.__INITIAL_STATE__={"a":1}</script>` +104 // triple should be raw105 `<div>foo</div>` +106 `</body></html>`107 )108 })109110 it('renderToString with interpolation and context.rendered', async () => {111 const renderer = createRenderer({112 template: interpolateTemplate113 })114115 const context = {116 title: '<script>hacks</script>',117 snippet: '<div>foo</div>',118 head: '<meta name="viewport" content="width=device-width">',119 styles: '<style>h1 { color: red }</style>',120 state: { a: 0 },121 rendered: context => {122 context.state.a = 1123 }124 }125126 const res = await renderer.renderToString(127 new Vue({128 template: '<div>hi</div>'129 }),130 context131 )132 expect(res).toContain(133 `<html><head>` +134 // double mustache should be escaped135 `<title><script>hacks</script></title>` +136 `${context.head}${context.styles}</head><body>` +137 `<div data-server-rendered="true">hi</div>` +138 `<script>window.__INITIAL_STATE__={"a":1}</script>` +139 // triple should be raw140 `<div>foo</div>` +141 `</body></html>`142 )143 })144145 it('renderToString w/ template function', async () => {146 const renderer = createRenderer({147 template: (content, context) =>148 `<html><head>${context.head}</head>${content}</html>`149 })150151 const context = {152 head: '<meta name="viewport" content="width=device-width">'153 }154155 const res = await renderer.renderToString(156 new Vue({157 template: '<div>hi</div>'158 }),159 context160 )161162 expect(res).toContain(163 `<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`164 )165 })166167 it('renderToString w/ template function returning Promise', async () => {168 const renderer = createRenderer({169 template: (content, context) =>170 new Promise<string>(resolve => {171 setTimeout(() => {172 resolve(`<html><head>${context.head}</head>${content}</html>`)173 }, 0)174 })175 })176177 const context = {178 head: '<meta name="viewport" content="width=device-width">'179 }180181 const res = await renderer.renderToString(182 new Vue({183 template: '<div>hi</div>'184 }),185 context186 )187188 expect(res).toContain(189 `<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`190 )191 })192193 it('renderToString w/ template function returning Promise w/ rejection', async () => {194 const renderer = createRenderer({195 template: () =>196 new Promise((resolve, reject) => {197 setTimeout(() => {198 reject(new Error(`foo`))199 }, 0)200 })201 })202203 const context = {204 head: '<meta name="viewport" content="width=device-width">'205 }206207 try {208 await renderer.renderToString(209 new Vue({210 template: '<div>hi</div>'211 }),212 context213 )214 } catch (err: any) {215 expect(err.message).toBe(`foo`)216 }217 })218219 it('renderToStream', async () => {220 const renderer = createRenderer({221 template: defaultTemplate222 })223224 const context = {225 head: '<meta name="viewport" content="width=device-width">',226 styles: '<style>h1 { color: red }</style>',227 state: { a: 1 }228 }229230 const res = await new Promise((resolve, reject) => {231 const stream = renderer.renderToStream(232 new Vue({233 template: '<div>hi</div>'234 }),235 context236 )237238 let res = ''239 stream.on('data', chunk => {240 res += chunk241 })242 stream.on('error', reject)243 stream.on('end', () => {244 resolve(res)245 })246 })247248 expect(res).toContain(249 `<html><head>${context.head}${context.styles}</head><body>` +250 `<div data-server-rendered="true">hi</div>` +251 `<script>window.__INITIAL_STATE__={"a":1}</script>` +252 `</body></html>`253 )254 })255256 it('renderToStream with interpolation', async () => {257 const renderer = createRenderer({258 template: interpolateTemplate259 })260261 const context = {262 title: '<script>hacks</script>',263 snippet: '<div>foo</div>',264 head: '<meta name="viewport" content="width=device-width">',265 styles: '<style>h1 { color: red }</style>',266 state: { a: 1 }267 }268269 const res = await new Promise((resolve, reject) => {270 const stream = renderer.renderToStream(271 new Vue({272 template: '<div>hi</div>'273 }),274 context275 )276277 let res = ''278 stream.on('data', chunk => {279 res += chunk280 })281 stream.on('error', reject)282 stream.on('end', () => {283 resolve(res)284 })285 })286287 expect(res).toContain(288 `<html><head>` +289 // double mustache should be escaped290 `<title><script>hacks</script></title>` +291 `${context.head}${context.styles}</head><body>` +292 `<div data-server-rendered="true">hi</div>` +293 `<script>window.__INITIAL_STATE__={"a":1}</script>` +294 // triple should be raw295 `<div>foo</div>` +296 `</body></html>`297 )298 })299300 it('renderToStream with interpolation and context.rendered', async () => {301 const renderer = createRenderer({302 template: interpolateTemplate303 })304305 const context = {306 title: '<script>hacks</script>',307 snippet: '<div>foo</div>',308 head: '<meta name="viewport" content="width=device-width">',309 styles: '<style>h1 { color: red }</style>',310 state: { a: 0 },311 rendered: context => {312 context.state.a = 1313 }314 }315316 const res = await new Promise((resolve, reject) => {317 const stream = renderer.renderToStream(318 new Vue({319 template: '<div>hi</div>'320 }),321 context322 )323324 let res = ''325 stream.on('data', chunk => {326 res += chunk327 })328 stream.on('error', reject)329 stream.on('end', () => {330 resolve(res)331 })332 })333334 expect(res).toContain(335 `<html><head>` +336 // double mustache should be escaped337 `<title><script>hacks</script></title>` +338 `${context.head}${context.styles}</head><body>` +339 `<div data-server-rendered="true">hi</div>` +340 `<script>window.__INITIAL_STATE__={"a":1}</script>` +341 // triple should be raw342 `<div>foo</div>` +343 `</body></html>`344 )345 })346347 it('bundleRenderer + renderToString', async () => {348 const renderer = await createWebpackBundleRenderer('app.js', {349 asBundle: true,350 template: defaultTemplate351 })352 const context: any = {353 head: '<meta name="viewport" content="width=device-width">',354 styles: '<style>h1 { color: red }</style>',355 state: { a: 1 },356 url: '/test'357 }358 const res = await renderer.renderToString(context)359 expect(res).toContain(360 `<html><head>${context.head}${context.styles}</head><body>` +361 `<div data-server-rendered="true">/test</div>` +362 `<script>window.__INITIAL_STATE__={"a":1}</script>` +363 `</body></html>`364 )365 expect(context.msg).toBe('hello')366 })367368 it('bundleRenderer + renderToStream', async () => {369 const renderer = await createWebpackBundleRenderer('app.js', {370 asBundle: true,371 template: defaultTemplate372 })373 const context: any = {374 head: '<meta name="viewport" content="width=device-width">',375 styles: '<style>h1 { color: red }</style>',376 state: { a: 1 },377 url: '/test'378 }379380 const res = await new Promise(resolve => {381 const stream = renderer.renderToStream(context)382 let res = ''383 stream.on('data', chunk => {384 res += chunk.toString()385 })386 stream.on('end', () => {387 resolve(res)388 })389 })390391 expect(res).toContain(392 `<html><head>${context.head}${context.styles}</head><body>` +393 `<div data-server-rendered="true">/test</div>` +394 `<script>window.__INITIAL_STATE__={"a":1}</script>` +395 `</body></html>`396 )397 expect(context.msg).toBe('hello')398 })399400 const expectedHTMLWithManifest = (options: any = {}) =>401 `<html><head>` +402 // used chunks should have preload403 `<link rel="preload" href="/manifest.js" as="script">` +404 `<link rel="preload" href="/main.js" as="script">` +405 `<link rel="preload" href="/0.js" as="script">` +406 `<link rel="preload" href="/test.css" as="style">` +407 // images and fonts are only preloaded when explicitly asked for408 (options.preloadOtherAssets409 ? `<link rel="preload" href="/test.png" as="image">`410 : ``) +411 (options.preloadOtherAssets412 ? `<link rel="preload" href="/test.woff2" as="font" type="font/woff2" crossorigin>`413 : ``) +414 // unused chunks should have prefetch415 (options.noPrefetch ? `` : `<link rel="prefetch" href="/1.js">`) +416 // css assets should be loaded417 `<link rel="stylesheet" href="/test.css">` +418 `</head><body>` +419 `<div data-server-rendered="true"><div>async test.woff2 test.png</div></div>` +420 // state should be inlined before scripts421 `<script>window.${422 options.stateKey || '__INITIAL_STATE__'423 }={"a":1}</script>` +424 // manifest chunk should be first425 `<script src="/manifest.js" defer></script>` +426 // async chunks should be before main chunk427 `<script src="/0.js" defer></script>` +428 `<script src="/main.js" defer></script>` +429 `</body></html>`430431 createClientManifestAssertions(true)432 createClientManifestAssertions(false)433434 function createClientManifestAssertions(runInNewContext) {435 it('bundleRenderer + renderToString + clientManifest ()', async () => {436 const renderer = await createRendererWithManifest('split.js', {437 runInNewContext438 })439 const res = await renderer.renderToString({ state: { a: 1 } })440 expect(res).toContain(expectedHTMLWithManifest())441 })442443 it('bundleRenderer + renderToStream + clientManifest + shouldPreload', async () => {444 const renderer = await createRendererWithManifest('split.js', {445 runInNewContext,446 shouldPreload: (file, type) => {447 if (448 type === 'image' ||449 type === 'script' ||450 type === 'font' ||451 type === 'style'452 ) {453 return true454 }455 }456 })457 const res = await new Promise(resolve => {458 const stream = renderer.renderToStream({ state: { a: 1 } })459 let res = ''460 stream.on('data', chunk => {461 res += chunk.toString()462 })463 stream.on('end', () => {464 resolve(res)465 })466 })467468 expect(res).toContain(469 expectedHTMLWithManifest({470 preloadOtherAssets: true471 })472 )473 })474475 it('bundleRenderer + renderToStream + clientManifest + shouldPrefetch', async () => {476 const renderer = await createRendererWithManifest('split.js', {477 runInNewContext,478 shouldPrefetch: (file, type) => {479 if (type === 'script') {480 return false481 }482 }483 })484485 const res = await new Promise(resolve => {486 const stream = renderer.renderToStream({ state: { a: 1 } })487 let res = ''488 stream.on('data', chunk => {489 res += chunk.toString()490 })491 stream.on('end', () => {492 resolve(res)493 })494 })495496 expect(res).toContain(497 expectedHTMLWithManifest({498 noPrefetch: true499 })500 )501 })502503 it('bundleRenderer + renderToString + clientManifest + inject: false', async () => {504 const renderer = await createRendererWithManifest('split.js', {505 runInNewContext,506 template:507 `<html>` +508 `<head>{{{ renderResourceHints() }}}{{{ renderStyles() }}}</head>` +509 `<body><!--vue-ssr-outlet-->{{{ renderState({ windowKey: '__FOO__', contextKey: 'foo' }) }}}{{{ renderScripts() }}}</body>` +510 `</html>`,511 inject: false512 })513 const context = { foo: { a: 1 } }514 const res = await renderer.renderToString(context)515 expect(res).toContain(516 expectedHTMLWithManifest({517 stateKey: '__FOO__'518 })519 )520 })521522 it('bundleRenderer + renderToString + clientManifest + no template', async () => {523 const renderer = await createRendererWithManifest('split.js', {524 runInNewContext,525 template: null as any526 })527 const context: any = { foo: { a: 1 } }528 const res = await renderer.renderToString(context)529530 const customOutput = `<html><head>${531 context.renderResourceHints() + context.renderStyles()532 }</head><body>${533 res +534 context.renderState({535 windowKey: '__FOO__',536 contextKey: 'foo'537 }) +538 context.renderScripts()539 }</body></html>`540541 expect(customOutput).toContain(542 expectedHTMLWithManifest({543 stateKey: '__FOO__'544 })545 )546 })547548 it('whitespace insensitive interpolation', async () => {549 const interpolateTemplate = `<html><head><title>{{title}}</title></head><body><!--vue-ssr-outlet-->{{{snippet}}}</body></html>`550 const renderer = createRenderer({551 template: interpolateTemplate552 })553554 const context = {555 title: '<script>hacks</script>',556 snippet: '<div>foo</div>',557 head: '<meta name="viewport" content="width=device-width">',558 styles: '<style>h1 { color: red }</style>',559 state: { a: 1 }560 }561562 const res = await renderer.renderToString(563 new Vue({564 template: '<div>hi</div>'565 }),566 context567 )568 expect(res).toContain(569 `<html><head>` +570 // double mustache should be escaped571 `<title><script>hacks</script></title>` +572 `${context.head}${context.styles}</head><body>` +573 `<div data-server-rendered="true">hi</div>` +574 `<script>window.__INITIAL_STATE__={"a":1}</script>` +575 // triple should be raw576 `<div>foo</div>` +577 `</body></html>`578 )579 })580581 it('renderToString + nonce', async () => {582 const interpolateTemplate = `<html><head><title>hello</title></head><body><!--vue-ssr-outlet--></body></html>`583 const renderer = createRenderer({584 template: interpolateTemplate585 })586587 const context = {588 state: { a: 1 },589 nonce: '4AEemGb0xJptoIGFP3Nd'590 }591592 const res = await renderer.renderToString(593 new Vue({594 template: '<div>hi</div>'595 }),596 context597 )598 expect(res).toContain(599 `<html><head>` +600 `<title>hello</title>` +601 `</head><body>` +602 `<div data-server-rendered="true">hi</div>` +603 `<script nonce="4AEemGb0xJptoIGFP3Nd">window.__INITIAL_STATE__={"a":1}</script>` +604 `</body></html>`605 )606 })607608 it('renderToString + custom serializer', async () => {609 const expected = `{"foo":123}`610 const renderer = createRenderer({611 template: defaultTemplate,612 serializer: () => expected613 })614615 const context = {616 state: { a: 1 }617 }618619 const res = await renderer.renderToString(620 new Vue({621 template: '<div>hi</div>'622 }),623 context624 )625 expect(res).toContain(626 `<script>window.__INITIAL_STATE__=${expected}</script>`627 )628 })629 }630})
Findings
✓ No findings reported for this file.