test: all ssr tests passing

This commit is contained in:
Evan You 2022-05-20 16:55:05 +08:00
parent 3a27b6b3e1
commit 898d1e19fc
25 changed files with 2401 additions and 772 deletions

View File

@ -26,6 +26,25 @@ jobs:
- name: Run unit tests
run: pnpm run test:unit
ssr-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Set node version to 16
uses: actions/setup-node@v2
with:
node-version: 16
cache: 'pnpm'
- run: pnpm install
- name: Run SSR tests
run: pnpm run test:ssr
# e2e-test:
# runs-on: ubuntu-latest
# steps:

View File

@ -23,7 +23,8 @@
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"test": "npm run lint && npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e",
"test:unit": "vitest run",
"test:unit": "vitest run test/unit",
"test:ssr": "vitest run test/ssr",
"test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.ts",
"test:types": "tsc -p ./types/tsconfig.json",
"lint": "eslint src scripts test",
@ -75,7 +76,7 @@
"de-indent": "^1.0.2",
"escodegen": "^2.0.0",
"eslint": "^8.14.0",
"file-loader": "^6.2.0",
"file-loader": "^3.0.1",
"fsevents": "2.3.2",
"hash-sum": "^2.0.0",
"he": "^1.2.0",
@ -99,7 +100,7 @@
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vitest": "^0.12.6",
"webpack": "^5.72.1",
"webpack": "^4.46.0",
"yorkie": "^2.0.0"
},
"config": {

File diff suppressed because it is too large Load Diff

View File

@ -236,8 +236,7 @@ function genConfig(name) {
// built-in vars
const vars = {
__VERSION__: version,
__SSR_TEST__: false
__VERSION__: version
}
// feature flags
Object.keys(featureFlags).forEach((key) => {

View File

@ -73,7 +73,7 @@ function logError(err, vm, info) {
warn(`Error in ${info}: "${err.toString()}"`, vm)
}
/* istanbul ignore else */
if (inBrowser && typeof console !== 'undefined' && !__SSR_TEST__) {
if (inBrowser && typeof console !== 'undefined') {
console.error(err)
} else {
throw err

2
src/global.d.ts vendored
View File

@ -1,5 +1,3 @@
declare const __SSR_TEST__: boolean;
interface Window {
__VUE_DEVTOOLS_GLOBAL_HOOK__: DevtoolsHook;
}

View File

@ -1,10 +1,9 @@
import { createPromiseCallback } from '../util'
import { createBundleRunner } from './create-bundle-runner'
import type { Renderer, RenderOptions } from '../create-renderer'
import {
createSourceMapConsumers,
rewriteErrorTrace,
rewriteErrorTrace
} from './source-map-support'
const fs = require('fs')
@ -89,7 +88,7 @@ export function createBundleRendererCreator(
)
return {
renderToString: (context: Object | undefined, cb: any) => {
renderToString: (context?: Object | undefined, cb?: any) => {
if (typeof context === 'function') {
cb = context
context = {}
@ -97,7 +96,7 @@ export function createBundleRendererCreator(
let promise
if (!cb) {
({ promise, cb } = createPromiseCallback())
;({ promise, cb } = createPromiseCallback())
}
run(context)
@ -154,7 +153,7 @@ export function createBundleRendererCreator(
})
return res
},
}
}
}
}

View File

@ -1,4 +1,3 @@
import RenderStream from './render-stream'
import { createWriteFunction } from './write'
import { createRenderFunction } from './render'
@ -29,7 +28,9 @@ export type RenderOptions = {
directives?: Object
isUnaryTag?: Function
cache?: RenderCache
template?: string | ((content: string, context: any) => string)
template?:
| string
| ((content: string, context: any) => string | Promise<string>)
inject?: boolean
basedir?: string
shouldPreload?: Function
@ -49,7 +50,7 @@ export function createRenderer({
shouldPreload,
shouldPrefetch,
clientManifest,
serializer,
serializer
}: RenderOptions = {}): Renderer {
const render = createRenderFunction(modules, directives, isUnaryTag, cache)
const templateRenderer = new TemplateRenderer({
@ -60,7 +61,7 @@ export function createRenderer({
// @ts-expect-error
shouldPrefetch,
clientManifest,
serializer,
serializer
})
return {
@ -80,7 +81,7 @@ export function createRenderer({
// no callback, return Promise
let promise
if (!cb) {
({ promise, cb } = createPromiseCallback())
;({ promise, cb } = createPromiseCallback())
}
let result = ''
@ -158,6 +159,6 @@ export function createRenderer({
}
return templateStream
}
},
}
}
}

View File

@ -1,4 +1,3 @@
const path = require('path')
const serialize = require('serialize-javascript')
@ -10,7 +9,9 @@ import type { ParsedTemplate } from './parse-template'
import type { AsyncFileMapper } from './create-async-file-mapper'
type TemplateRendererOptions = {
template?: string | ((content: string, context: any) => string)
template?:
| string
| ((content: string, context: any) => string | Promise<string>)
inject?: boolean
clientManifest?: ClientManifest
shouldPreload?: (file: string, type: string) => boolean
@ -286,7 +287,7 @@ function normalizeFile(file: string): Resource {
file,
extension,
fileWithoutQuery: withoutQuery,
asType: getPreloadType(extension),
asType: getPreloadType(extension)
}
}

View File

@ -1,9 +1,15 @@
import path from 'path'
import webpack from 'webpack'
import MemoryFS from 'memory-fs'
import { RenderOptions } from '../../src/server/create-renderer'
import { createBundleRenderer } from 'web/entry-server-renderer'
import VueSSRServerPlugin from 'server/webpack-plugin/server'
export function compileWithWebpack (file, extraConfig, cb) {
const config = Object.assign({
export function compileWithWebpack(
file: string,
extraConfig?: webpack.Configuration
) {
const config: webpack.Configuration = {
mode: 'development',
entry: path.resolve(__dirname, 'fixtures', file),
module: {
@ -21,15 +27,47 @@ export function compileWithWebpack (file, extraConfig, cb) {
}
]
}
}, extraConfig)
}
if (extraConfig) {
Object.assign(config, extraConfig)
}
const compiler = webpack(config)
const fs = new MemoryFS()
compiler.outputFileSystem = fs
compiler.run((err, stats) => {
expect(err).toBeFalsy()
expect(stats.errors).toBeFalsy()
cb(fs)
return new Promise<MemoryFS>((resolve, reject) => {
compiler.run((err) => {
if (err) {
reject(err)
} else {
resolve(fs)
}
})
})
}
export async function createWebpackBundleRenderer(
file: string,
options?: RenderOptions & { asBundle?: boolean }
) {
const asBundle = !!(options && options.asBundle)
if (options) delete options.asBundle
const fs = await compileWithWebpack(file, {
target: 'node',
devtool: asBundle ? 'source-map' : false,
output: {
path: '/',
filename: 'bundle.js',
libraryTarget: 'commonjs2'
},
externals: [require.resolve('../../dist/vue.runtime.common.js')],
plugins: asBundle ? [new VueSSRServerPlugin()] : []
})
const bundle = asBundle
? JSON.parse(fs.readFileSync('/vue-ssr-server-bundle.json', 'utf-8'))
: fs.readFileSync('/bundle.js', 'utf-8')
return createBundleRenderer(bundle, options)
}

View File

@ -1,8 +1,8 @@
// @vitest-environment node
import Vue from 'vue'
import renderToString from 'web/entry-server-basic-renderer'
;(global as any).__SSR_TEST__ = true
describe('SSR: basicRenderer', () => {
it('should work', done => {
renderToString(new Vue({

View File

@ -1,112 +1,95 @@
// @vitest-environment node
import LRU from 'lru-cache'
import { compileWithWebpack } from './compile-with-webpack'
import { createBundleRenderer } from 'web/entry-server-renderer'
import VueSSRServerPlugin from 'server/webpack-plugin/server'
import { createWebpackBundleRenderer } from './compile-with-webpack'
;(global as any).__SSR_TEST__ = true
export function createRenderer (file, options, cb) {
if (typeof options === 'function') {
cb = options
options = undefined
}
const asBundle = !!(options && options.asBundle)
if (options) delete options.asBundle
compileWithWebpack(file, {
target: 'node',
devtool: asBundle ? 'source-map' : false,
output: {
path: '/',
filename: 'bundle.js',
libraryTarget: 'commonjs2'
},
externals: [require.resolve('../../dist/vue.runtime.common.js')],
plugins: asBundle
? [new VueSSRServerPlugin()]
: []
}, fs => {
const bundle = asBundle
? JSON.parse(fs.readFileSync('/vue-ssr-server-bundle.json', 'utf-8'))
: fs.readFileSync('/bundle.js', 'utf-8')
const renderer = createBundleRenderer(bundle, options)
cb(renderer)
})
}
describe.skip('SSR: bundle renderer', () => {
describe('SSR: bundle renderer', () => {
createAssertions(true)
createAssertions(false)
})
function createAssertions (runInNewContext) {
it('renderToString', done => {
createRenderer('app.js', { runInNewContext }, renderer => {
const context = { url: '/test' }
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
done()
})
function createAssertions(runInNewContext) {
it('renderToString', async () => {
const renderer = await createWebpackBundleRenderer('app.js', {
runInNewContext
})
const context: any = { url: '/test' }
const res = await renderer.renderToString(context)
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
})
it('renderToStream', done => {
createRenderer('app.js', { runInNewContext }, renderer => {
const context = { url: '/test' }
it('renderToStream', async () => {
const renderer = await createWebpackBundleRenderer('app.js', {
runInNewContext
})
const context: any = { url: '/test' }
const res = await new Promise((resolve, reject) => {
const stream = renderer.renderToStream(context)
let res = ''
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res += chunk.toString()
})
stream.on('error', reject)
stream.on('end', () => {
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
done()
resolve(res)
})
})
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
})
it('renderToString catch error', done => {
createRenderer('error.js', { runInNewContext }, renderer => {
renderer.renderToString(err => {
expect(err.message).toBe('foo')
done()
})
it('renderToString catch error', async () => {
const renderer = await createWebpackBundleRenderer('error.js', {
runInNewContext
})
try {
await renderer.renderToString()
} catch (err: any) {
expect(err.message).toBe('foo')
}
})
it('renderToString catch Promise rejection', done => {
createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
renderer.renderToString(err => {
expect(err.message).toBe('foo')
done()
})
it('renderToString catch Promise rejection', async () => {
const renderer = await createWebpackBundleRenderer('promise-rejection.js', {
runInNewContext
})
try {
await renderer.renderToString()
} catch (err: any) {
expect(err.message).toBe('foo')
}
})
it('renderToStream catch error', done => {
createRenderer('error.js', { runInNewContext }, renderer => {
it('renderToStream catch error', async () => {
const renderer = await createWebpackBundleRenderer('error.js', {
runInNewContext
})
const err = await new Promise<Error>((resolve) => {
const stream = renderer.renderToStream()
stream.on('error', err => {
expect(err.message).toBe('foo')
done()
})
stream.on('error', resolve)
})
expect(err.message).toBe('foo')
})
it('renderToStream catch Promise rejection', done => {
createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
it('renderToStream catch Promise rejection', async () => {
const renderer = await createWebpackBundleRenderer('promise-rejection.js', {
runInNewContext
})
const err = await new Promise<Error>((resolve) => {
const stream = renderer.renderToStream()
stream.on('error', err => {
expect(err.message).toBe('foo')
done()
})
stream.on('error', resolve)
})
expect(err.message).toBe('foo')
})
it('render with cache (get/set)', done => {
it('render with cache (get/set)', async () => {
const cache = {}
const get = vi.fn()
const set = vi.fn()
@ -126,29 +109,25 @@ function createAssertions (runInNewContext) {
}
}
}
createRenderer('cache.js', options, renderer => {
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get).toHaveBeenCalledWith(key)
const setArgs = set.mock.calls[0]
expect(setArgs[0]).toBe(key)
expect(setArgs[1].html).toBe(expected)
expect(cache[key].html).toBe(expected)
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get.mock.calls.length).toBe(2)
expect(set.mock.calls.length).toBe(1)
done()
})
})
})
const renderer = await createWebpackBundleRenderer('cache.js', options)
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
const res = await renderer.renderToString()
expect(res).toBe(expected)
expect(get).toHaveBeenCalledWith(key)
const setArgs = set.mock.calls[0]
expect(setArgs[0]).toBe(key)
expect(setArgs[1].html).toBe(expected)
expect(cache[key].html).toBe(expected)
const res2 = await renderer.renderToString()
expect(res2).toBe(expected)
expect(get.mock.calls.length).toBe(2)
expect(set.mock.calls.length).toBe(1)
})
it('render with cache (get/set/has)', done => {
it('render with cache (get/set/has)', async () => {
const cache = {}
const has = vi.fn()
const get = vi.fn()
@ -162,7 +141,7 @@ function createAssertions (runInNewContext) {
cb(!!cache[key])
},
// sync
get: key => {
get: (key) => {
get(key)
return cache[key]
},
@ -172,68 +151,60 @@ function createAssertions (runInNewContext) {
}
}
}
createRenderer('cache.js', options, renderer => {
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(has).toHaveBeenCalledWith(key)
expect(get).not.toHaveBeenCalled()
const setArgs = set.mock.calls[0]
expect(setArgs[0]).toBe(key)
expect(setArgs[1].html).toBe(expected)
expect(cache[key].html).toBe(expected)
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(has.mock.calls.length).toBe(2)
expect(get.mock.calls.length).toBe(1)
expect(set.mock.calls.length).toBe(1)
done()
})
})
})
const renderer = await createWebpackBundleRenderer('cache.js', options)
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
const res = await renderer.renderToString()
expect(res).toBe(expected)
expect(has).toHaveBeenCalledWith(key)
expect(get).not.toHaveBeenCalled()
const setArgs = set.mock.calls[0]
expect(setArgs[0]).toBe(key)
expect(setArgs[1].html).toBe(expected)
expect(cache[key].html).toBe(expected)
const res2 = await renderer.renderToString()
expect(res2).toBe(expected)
expect(has.mock.calls.length).toBe(2)
expect(get.mock.calls.length).toBe(1)
expect(set.mock.calls.length).toBe(1)
})
it('render with cache (nested)', done => {
const cache = new LRU({ maxAge: Infinity })
spyOn(cache, 'get').and.callThrough()
spyOn(cache, 'set').and.callThrough()
it('render with cache (nested)', async () => {
const cache = new LRU({ ttl: 65535 }) as any
vi.spyOn(cache, 'get')
vi.spyOn(cache, 'set')
const options = {
cache,
runInNewContext
}
createRenderer('nested-cache.js', options, renderer => {
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
const context1 = { registered: [] }
const context2 = { registered: [] }
renderer.renderToString(context1, (err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(cache.set.mock.calls.length).toBe(3) // 3 nested components cached
const cached = cache.get(key)
expect(cached.html).toBe(expected)
expect(cache.get.mock.calls.length).toBe(1)
const renderer = await createWebpackBundleRenderer(
'nested-cache.js',
options
)
const expected = '<div data-server-rendered="true">/test</div>'
const key = 'app::1'
const context1 = { registered: [] }
const context2 = { registered: [] }
const res = await renderer.renderToString(context1)
expect(res).toBe(expected)
expect(cache.set.mock.calls.length).toBe(3) // 3 nested components cached
const cached = cache.get(key)
expect(cached.html).toBe(expected)
expect(cache.get.mock.calls.length).toBe(1)
// assert component usage registration for nested children
expect(context1.registered).toEqual(['app', 'child', 'grandchild'])
// assert component usage registration for nested children
expect(context1.registered).toEqual(['app', 'child', 'grandchild'])
renderer.renderToString(context2, (err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(cache.set.mock.calls.length).toBe(3) // no new cache sets
expect(cache.get.mock.calls.length).toBe(2) // 1 get for root
const res2 = await renderer.renderToString(context2)
expect(res2).toBe(expected)
expect(cache.set.mock.calls.length).toBe(3) // no new cache sets
expect(cache.get.mock.calls.length).toBe(2) // 1 get for root
expect(context2.registered).toEqual(['app', 'child', 'grandchild'])
done()
})
})
})
expect(context2.registered).toEqual(['app', 'child', 'grandchild'])
})
it('render with cache (opt-out)', done => {
it('render with cache (opt-out)', async () => {
const cache = {}
const get = vi.fn()
const set = vi.fn()
@ -253,97 +224,103 @@ function createAssertions (runInNewContext) {
}
}
}
createRenderer('cache-opt-out.js', options, renderer => {
const expected = '<div data-server-rendered="true">/test</div>'
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get).not.toHaveBeenCalled()
expect(set).not.toHaveBeenCalled()
renderer.renderToString((err, res) => {
expect(err).toBeNull()
expect(res).toBe(expected)
expect(get).not.toHaveBeenCalled()
expect(set).not.toHaveBeenCalled()
done()
})
})
})
const renderer = await createWebpackBundleRenderer(
'cache-opt-out.js',
options
)
const expected = '<div data-server-rendered="true">/test</div>'
const res = await renderer.renderToString()
expect(res).toBe(expected)
expect(get).not.toHaveBeenCalled()
expect(set).not.toHaveBeenCalled()
const res2 = await renderer.renderToString()
expect(res2).toBe(expected)
expect(get).not.toHaveBeenCalled()
expect(set).not.toHaveBeenCalled()
})
it('renderToString (bundle format with code split)', done => {
createRenderer('split.js', { runInNewContext, asBundle: true }, renderer => {
const context = { url: '/test' }
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
done()
})
it('renderToString (bundle format with code split)', async () => {
const renderer = await createWebpackBundleRenderer('split.js', {
runInNewContext,
asBundle: true
})
const context = { url: '/test' }
const res = await renderer.renderToString(context)
expect(res).toBe(
'<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>'
)
})
it('renderToStream (bundle format with code split)', done => {
createRenderer('split.js', { runInNewContext, asBundle: true }, renderer => {
const context = { url: '/test' }
it('renderToStream (bundle format with code split)', async () => {
const renderer = await createWebpackBundleRenderer('split.js', {
runInNewContext,
asBundle: true
})
const context = { url: '/test' }
const res = await new Promise((resolve, reject) => {
const stream = renderer.renderToStream(context)
let res = ''
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res += chunk.toString()
})
stream.on('error', reject)
stream.on('end', () => {
expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
done()
resolve(res)
})
})
expect(res).toBe(
'<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>'
)
})
it('renderToString catch error (bundle format with source map)', done => {
createRenderer('error.js', { runInNewContext, asBundle: true }, renderer => {
renderer.renderToString(err => {
expect(err.stack).toContain('test/ssr/fixtures/error.js:1:6')
expect(err.message).toBe('foo')
done()
})
it('renderToString catch error (bundle format with source map)', async () => {
const renderer = await createWebpackBundleRenderer('error.js', {
runInNewContext,
asBundle: true
})
try {
await renderer.renderToString()
} catch (err: any) {
expect(err.stack).toContain('test/ssr/fixtures/error.js:1:0')
expect(err.message).toBe('foo')
}
})
it('renderToString catch error (bundle format with source map)', done => {
createRenderer('error.js', { runInNewContext, asBundle: true }, renderer => {
it('renderToStream catch error (bundle format with source map)', async () => {
const renderer = await createWebpackBundleRenderer('error.js', {
runInNewContext,
asBundle: true
})
const err = await new Promise<Error>((resolve) => {
const stream = renderer.renderToStream()
stream.on('error', err => {
expect(err.stack).toContain('test/ssr/fixtures/error.js:1:6')
expect(err.message).toBe('foo')
done()
})
stream.on('error', resolve)
})
expect(err.stack).toContain('test/ssr/fixtures/error.js:1:0')
expect(err.message).toBe('foo')
})
it('renderToString return Promise', done => {
createRenderer('app.js', { runInNewContext }, renderer => {
const context = { url: '/test' }
renderer.renderToString(context).then(res => {
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
done()
})
it('renderToString w/ callback', async () => {
const renderer = await createWebpackBundleRenderer('app.js', {
runInNewContext
})
const context: any = { url: '/test' }
const res = await new Promise((r) =>
renderer.renderToString(context, (_err, res) => r(res))
)
expect(res).toBe('<div data-server-rendered="true">/test</div>')
expect(context.msg).toBe('hello')
})
it('renderToString return Promise (error)', done => {
createRenderer('error.js', { runInNewContext }, renderer => {
renderer.renderToString().catch(err => {
expect(err.message).toBe('foo')
done()
})
})
})
it('renderToString return Promise (Promise rejection)', done => {
createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
renderer.renderToString().catch(err => {
expect(err.message).toBe('foo')
done()
})
it('renderToString error handling w/ callback', async () => {
const renderer = await createWebpackBundleRenderer('error.js', {
runInNewContext
})
const err = await new Promise<Error>((r) => renderer.renderToString(r))
expect(err.message).toBe('foo')
})
}

View File

@ -1,9 +1,9 @@
// @vitest-environment node
import Vue from 'vue'
import { createRenderer } from 'web/entry-server-renderer'
const { renderToStream } = createRenderer()
;(global as any).__SSR_TEST__ = true
describe('SSR: renderToStream', () => {
it('should render to a stream', done => {
const stream = renderToStream(new Vue({

View File

@ -1,10 +1,10 @@
// @vitest-environment node
import Vue from 'vue'
import VM from 'vm'
import { createRenderer } from 'web/entry-server-renderer'
const { renderToString } = createRenderer()
;(global as any).__SSR_TEST__ = true
describe('SSR: renderToString', () => {
it('static attributes', done => {
renderVmWithOptions({

View File

@ -1,16 +1,19 @@
// @vitest-environment node
import Vue from 'vue'
import { compileWithWebpack } from './compile-with-webpack'
import {
compileWithWebpack,
createWebpackBundleRenderer
} from './compile-with-webpack'
import { createRenderer } from 'web/entry-server-renderer'
import VueSSRClientPlugin from 'server/webpack-plugin/client'
import { createRenderer as createBundleRenderer } from './ssr-bundle-render.spec.js'
;(global as any).__SSR_TEST__ = true
import { RenderOptions } from '../../src/server/create-renderer'
const defaultTemplate = `<html><head></head><body><!--vue-ssr-outlet--></body></html>`
const interpolateTemplate = `<html><head><title>{{ title }}</title></head><body><!--vue-ssr-outlet-->{{{ snippet }}}</body></html>`
function generateClientManifest (file, cb) {
compileWithWebpack(file, {
async function generateClientManifest(file: string) {
const fs = await compileWithWebpack(file, {
output: {
path: '/',
publicPath: '/',
@ -21,30 +24,31 @@ function generateClientManifest (file, cb) {
name: 'manifest'
}
},
plugins: [
new VueSSRClientPlugin()
]
}, fs => {
cb(JSON.parse(fs.readFileSync('/vue-ssr-client-manifest.json', 'utf-8')))
plugins: [new VueSSRClientPlugin()]
})
return JSON.parse(fs.readFileSync('/vue-ssr-client-manifest.json', 'utf-8'))
}
function createRendererWithManifest (file, options, cb) {
if (typeof options === 'function') {
cb = options
options = null
}
generateClientManifest(file, clientManifest => {
createBundleRenderer(file, Object.assign({
asBundle: true,
template: defaultTemplate,
clientManifest
}, options), cb)
})
async function createRendererWithManifest(
file: string,
options?: RenderOptions
) {
const clientManifest = await generateClientManifest(file)
return createWebpackBundleRenderer(
file,
Object.assign(
{
asBundle: true,
template: defaultTemplate,
clientManifest
},
options
)
)
}
describe.skip('SSR: template option', () => {
it('renderToString', done => {
describe('SSR: template option', () => {
it('renderToString', async () => {
const renderer = createRenderer({
template: defaultTemplate
})
@ -55,21 +59,22 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">hi</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
done()
})
)
})
it('renderToString with interpolation', done => {
it('renderToString with interpolation', async () => {
const renderer = createRenderer({
template: interpolateTemplate
})
@ -82,12 +87,15 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>` +
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>` +
// double mustache should be escaped
`<title>&lt;script&gt;hacks&lt;/script&gt;</title>` +
`${context.head}${context.styles}</head><body>` +
@ -96,12 +104,10 @@ describe.skip('SSR: template option', () => {
// triple should be raw
`<div>foo</div>` +
`</body></html>`
)
done()
})
)
})
it('renderToString with interpolation and context.rendered', done => {
it('renderToString with interpolation and context.rendered', async () => {
const renderer = createRenderer({
template: interpolateTemplate
})
@ -112,17 +118,19 @@ describe.skip('SSR: template option', () => {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 0 },
rendered: context => {
rendered: (context) => {
context.state.a = 1
}
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>` +
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>` +
// double mustache should be escaped
`<title>&lt;script&gt;hacks&lt;/script&gt;</title>` +
`${context.head}${context.styles}</head><body>` +
@ -131,74 +139,84 @@ describe.skip('SSR: template option', () => {
// triple should be raw
`<div>foo</div>` +
`</body></html>`
)
})
it('renderToString w/ template function', async () => {
const renderer = createRenderer({
template: (content, context) =>
`<html><head>${context.head}</head>${content}</html>`
})
const context = {
head: '<meta name="viewport" content="width=device-width">'
}
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`
)
})
it('renderToString w/ template function returning Promise', async () => {
const renderer = createRenderer({
template: (content, context) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`<html><head>${context.head}</head>${content}</html>`)
}, 0)
})
})
const context = {
head: '<meta name="viewport" content="width=device-width">'
}
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`
)
})
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: '<meta name="viewport" content="width=device-width">'
}
try {
await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
done()
})
})
it('renderToString w/ template function', done => {
const renderer = createRenderer({
template: (content, context) => `<html><head>${context.head}</head>${content}</html>`
})
const context = {
head: '<meta name="viewport" content="width=device-width">'
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(`<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`)
done()
})
})
it('renderToString w/ template function returning Promise', done => {
const renderer = createRenderer({
template: (content, context) => new Promise((resolve) => {
setTimeout(() => {
resolve(`<html><head>${context.head}</head>${content}</html>`)
}, 0)
})
})
const context = {
head: '<meta name="viewport" content="width=device-width">'
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(`<html><head>${context.head}</head><div data-server-rendered="true">hi</div></html>`)
done()
})
})
it('renderToString w/ template function returning Promise w/ rejection', done => {
const renderer = createRenderer({
template: () => new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error(`foo`))
}, 0)
})
})
const context = {
head: '<meta name="viewport" content="width=device-width">'
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
} catch (err: any) {
expect(err.message).toBe(`foo`)
expect(res).toBeUndefined()
done()
})
}
})
it('renderToStream', done => {
it('renderToStream', async () => {
const renderer = createRenderer({
template: defaultTemplate
})
@ -209,26 +227,33 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
const stream = renderer.renderToStream(new Vue({
template: '<div>hi</div>'
}), context)
const res = await new Promise((resolve, reject) => {
const stream = renderer.renderToStream(
new Vue({
template: '<div>hi</div>'
}),
context
)
let res = ''
stream.on('data', chunk => {
res += chunk
let res = ''
stream.on('data', (chunk) => {
res += chunk
})
stream.on('error', reject)
stream.on('end', () => {
resolve(res)
})
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">hi</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
done()
})
)
})
it('renderToStream with interpolation', done => {
it('renderToStream with interpolation', async () => {
const renderer = createRenderer({
template: interpolateTemplate
})
@ -241,17 +266,26 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
const stream = renderer.renderToStream(new Vue({
template: '<div>hi</div>'
}), context)
const res = await new Promise((resolve, reject) => {
const stream = renderer.renderToStream(
new Vue({
template: '<div>hi</div>'
}),
context
)
let res = ''
stream.on('data', chunk => {
res += chunk
let res = ''
stream.on('data', (chunk) => {
res += chunk
})
stream.on('error', reject)
stream.on('end', () => {
resolve(res)
})
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>` +
expect(res).toContain(
`<html><head>` +
// double mustache should be escaped
`<title>&lt;script&gt;hacks&lt;/script&gt;</title>` +
`${context.head}${context.styles}</head><body>` +
@ -260,12 +294,10 @@ describe.skip('SSR: template option', () => {
// triple should be raw
`<div>foo</div>` +
`</body></html>`
)
done()
})
)
})
it('renderToStream with interpolation and context.rendered', done => {
it('renderToStream with interpolation and context.rendered', async () => {
const renderer = createRenderer({
template: interpolateTemplate
})
@ -276,22 +308,31 @@ describe.skip('SSR: template option', () => {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 0 },
rendered: context => {
rendered: (context) => {
context.state.a = 1
}
}
const stream = renderer.renderToStream(new Vue({
template: '<div>hi</div>'
}), context)
const res = await new Promise((resolve, reject) => {
const stream = renderer.renderToStream(
new Vue({
template: '<div>hi</div>'
}),
context
)
let res = ''
stream.on('data', chunk => {
res += chunk
let res = ''
stream.on('data', (chunk) => {
res += chunk
})
stream.on('error', reject)
stream.on('end', () => {
resolve(res)
})
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>` +
expect(res).toContain(
`<html><head>` +
// double mustache should be escaped
`<title>&lt;script&gt;hacks&lt;/script&gt;</title>` +
`${context.head}${context.styles}</head><body>` +
@ -300,201 +341,211 @@ describe.skip('SSR: template option', () => {
// triple should be raw
`<div>foo</div>` +
`</body></html>`
)
done()
})
)
})
it('bundleRenderer + renderToString', done => {
createBundleRenderer('app.js', {
it('bundleRenderer + renderToString', async () => {
const renderer = await createWebpackBundleRenderer('app.js', {
asBundle: true,
template: defaultTemplate
}, renderer => {
const context = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
done()
})
})
const context: any = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
const res = await renderer.renderToString(context)
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
})
it('bundleRenderer + renderToStream', done => {
createBundleRenderer('app.js', {
it('bundleRenderer + renderToStream', async () => {
const renderer = await createWebpackBundleRenderer('app.js', {
asBundle: true,
template: defaultTemplate
}, renderer => {
const context = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
})
const context: any = {
head: '<meta name="viewport" content="width=device-width">',
styles: '<style>h1 { color: red }</style>',
state: { a: 1 },
url: '/test'
}
const res = await new Promise((resolve) => {
const stream = renderer.renderToStream(context)
let res = ''
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
done()
resolve(res)
})
})
expect(res).toContain(
`<html><head>${context.head}${context.styles}</head><body>` +
`<div data-server-rendered="true">/test</div>` +
`<script>window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
expect(context.msg).toBe('hello')
})
const expectedHTMLWithManifest = (options = {}) =>
const expectedHTMLWithManifest = (options: any = {}) =>
`<html><head>` +
// used chunks should have preload
`<link rel="preload" href="/manifest.js" as="script">` +
`<link rel="preload" href="/main.js" as="script">` +
`<link rel="preload" href="/0.js" as="script">` +
`<link rel="preload" href="/test.css" as="style">` +
// images and fonts are only preloaded when explicitly asked for
(options.preloadOtherAssets ? `<link rel="preload" href="/test.png" as="image">` : ``) +
(options.preloadOtherAssets ? `<link rel="preload" href="/test.woff2" as="font" type="font/woff2" crossorigin>` : ``) +
// unused chunks should have prefetch
(options.noPrefetch ? `` : `<link rel="prefetch" href="/1.js">`) +
// css assets should be loaded
`<link rel="stylesheet" href="/test.css">` +
// used chunks should have preload
`<link rel="preload" href="/manifest.js" as="script">` +
`<link rel="preload" href="/main.js" as="script">` +
`<link rel="preload" href="/0.js" as="script">` +
`<link rel="preload" href="/test.css" as="style">` +
// images and fonts are only preloaded when explicitly asked for
(options.preloadOtherAssets
? `<link rel="preload" href="/test.png" as="image">`
: ``) +
(options.preloadOtherAssets
? `<link rel="preload" href="/test.woff2" as="font" type="font/woff2" crossorigin>`
: ``) +
// unused chunks should have prefetch
(options.noPrefetch ? `` : `<link rel="prefetch" href="/1.js">`) +
// css assets should be loaded
`<link rel="stylesheet" href="/test.css">` +
`</head><body>` +
`<div data-server-rendered="true"><div>async test.woff2 test.png</div></div>` +
// state should be inlined before scripts
`<script>window.${options.stateKey || '__INITIAL_STATE__'}={"a":1}</script>` +
// manifest chunk should be first
`<script src="/manifest.js" defer></script>` +
// async chunks should be before main chunk
`<script src="/0.js" defer></script>` +
`<script src="/main.js" defer></script>` +
`<div data-server-rendered="true"><div>async test.woff2 test.png</div></div>` +
// state should be inlined before scripts
`<script>window.${
options.stateKey || '__INITIAL_STATE__'
}={"a":1}</script>` +
// manifest chunk should be first
`<script src="/manifest.js" defer></script>` +
// async chunks should be before main chunk
`<script src="/0.js" defer></script>` +
`<script src="/main.js" defer></script>` +
`</body></html>`
createClientManifestAssertions(true)
createClientManifestAssertions(false)
function createClientManifestAssertions (runInNewContext) {
it('bundleRenderer + renderToString + clientManifest ()', done => {
createRendererWithManifest('split.js', { runInNewContext }, renderer => {
renderer.renderToString({ state: { a: 1 }}, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(expectedHTMLWithManifest())
done()
})
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', done => {
createRendererWithManifest('split.js', {
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') {
if (
type === 'image' ||
type === 'script' ||
type === 'font' ||
type === 'style'
) {
return true
}
}
}, renderer => {
const stream = renderer.renderToStream({ state: { a: 1 }})
})
const res = await new Promise((resolve) => {
const stream = renderer.renderToStream({ state: { a: 1 } })
let res = ''
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toContain(expectedHTMLWithManifest({
preloadOtherAssets: true
}))
done()
resolve(res)
})
})
expect(res).toContain(
expectedHTMLWithManifest({
preloadOtherAssets: true
})
)
})
it('bundleRenderer + renderToStream + clientManifest + shouldPrefetch', done => {
createRendererWithManifest('split.js', {
it('bundleRenderer + renderToStream + clientManifest + shouldPrefetch', async () => {
const renderer = await createRendererWithManifest('split.js', {
runInNewContext,
shouldPrefetch: (file, type) => {
if (type === 'script') {
return false
}
}
}, renderer => {
const stream = renderer.renderToStream({ state: { a: 1 }})
})
const res = await new Promise((resolve) => {
const stream = renderer.renderToStream({ state: { a: 1 } })
let res = ''
stream.on('data', chunk => {
stream.on('data', (chunk) => {
res += chunk.toString()
})
stream.on('end', () => {
expect(res).toContain(expectedHTMLWithManifest({
noPrefetch: true
}))
done()
resolve(res)
})
})
expect(res).toContain(
expectedHTMLWithManifest({
noPrefetch: true
})
)
})
it('bundleRenderer + renderToString + clientManifest + inject: false', done => {
createRendererWithManifest('split.js', {
it('bundleRenderer + renderToString + clientManifest + inject: false', async () => {
const renderer = await createRendererWithManifest('split.js', {
runInNewContext,
template: `<html>` +
template:
`<html>` +
`<head>{{{ renderResourceHints() }}}{{{ renderStyles() }}}</head>` +
`<body><!--vue-ssr-outlet-->{{{ renderState({ windowKey: '__FOO__', contextKey: 'foo' }) }}}{{{ renderScripts() }}}</body>` +
`</html>`,
`</html>`,
inject: false
}, renderer => {
const context = { foo: { a: 1 }}
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(expectedHTMLWithManifest({
stateKey: '__FOO__'
}))
done()
})
})
const context = { foo: { a: 1 } }
const res = await renderer.renderToString(context)
expect(res).toContain(
expectedHTMLWithManifest({
stateKey: '__FOO__'
})
)
})
it('bundleRenderer + renderToString + clientManifest + no template', done => {
createRendererWithManifest('split.js', {
it('bundleRenderer + renderToString + clientManifest + no template', async () => {
const renderer = await createRendererWithManifest('split.js', {
runInNewContext,
template: null
}, renderer => {
const context = { foo: { a: 1 }}
renderer.renderToString(context, (err, res) => {
expect(err).toBeNull()
const customOutput =
`<html><head>${
context.renderResourceHints() +
context.renderStyles()
}</head><body>${
res +
context.renderState({
windowKey: '__FOO__',
contextKey: 'foo'
}) +
context.renderScripts()
}</body></html>`
expect(customOutput).toContain(expectedHTMLWithManifest({
stateKey: '__FOO__'
}))
done()
})
template: null as any
})
const context: any = { foo: { a: 1 } }
const res = await renderer.renderToString(context)
const customOutput = `<html><head>${
context.renderResourceHints() + context.renderStyles()
}</head><body>${
res +
context.renderState({
windowKey: '__FOO__',
contextKey: 'foo'
}) +
context.renderScripts()
}</body></html>`
expect(customOutput).toContain(
expectedHTMLWithManifest({
stateKey: '__FOO__'
})
)
})
it('whitespace insensitive interpolation', done => {
it('whitespace insensitive interpolation', async () => {
const interpolateTemplate = `<html><head><title>{{title}}</title></head><body><!--vue-ssr-outlet-->{{{snippet}}}</body></html>`
const renderer = createRenderer({
template: interpolateTemplate
@ -508,12 +559,14 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>` +
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>` +
// double mustache should be escaped
`<title>&lt;script&gt;hacks&lt;/script&gt;</title>` +
`${context.head}${context.styles}</head><body>` +
@ -522,12 +575,10 @@ describe.skip('SSR: template option', () => {
// triple should be raw
`<div>foo</div>` +
`</body></html>`
)
done()
})
)
})
it('renderToString + nonce', done => {
it('renderToString + nonce', async () => {
const interpolateTemplate = `<html><head><title>hello</title></head><body><!--vue-ssr-outlet--></body></html>`
const renderer = createRenderer({
template: interpolateTemplate
@ -538,23 +589,23 @@ describe.skip('SSR: template option', () => {
nonce: '4AEemGb0xJptoIGFP3Nd'
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<html><head>` +
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<html><head>` +
`<title>hello</title>` +
`</head><body>` +
`<div data-server-rendered="true">hi</div>` +
`<script nonce="4AEemGb0xJptoIGFP3Nd">window.__INITIAL_STATE__={"a":1}</script>` +
`</body></html>`
)
done()
})
)
})
it('renderToString + custom serializer', done => {
it('renderToString + custom serializer', async () => {
const expected = `{"foo":123}`
const renderer = createRenderer({
template: defaultTemplate,
@ -565,15 +616,15 @@ describe.skip('SSR: template option', () => {
state: { a: 1 }
}
renderer.renderToString(new Vue({
template: '<div>hi</div>'
}), context, (err, res) => {
expect(err).toBeNull()
expect(res).toContain(
`<script>window.__INITIAL_STATE__=${expected}</script>`
)
done()
})
const res = await renderer.renderToString(
new Vue({
template: '<div>hi</div>'
}),
context
)
expect(res).toContain(
`<script>window.__INITIAL_STATE__=${expected}</script>`
)
})
}
})

View File

@ -1,5 +1,3 @@
;(global as any).__SSR_TEST__ = false
process.env.NEW_SLOT_SYNTAX = 'true'
import './helpers/shim-done'