packages/server-renderer/test/ssr-template.spec.ts TYPESCRIPT 631 lines View on github.com → Search inside
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>&lt;script&gt;hacks&lt;/script&gt;</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>&lt;script&gt;hacks&lt;/script&gt;</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>&lt;script&gt;hacks&lt;/script&gt;</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>&lt;script&gt;hacks&lt;/script&gt;</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>&lt;script&gt;hacks&lt;/script&gt;</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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.