ssr-bundle-render.spec.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import LRU from 'lru-cache'
  2. import { compileWithWebpack } from './compile-with-webpack'
  3. import { createBundleRenderer } from '../../packages/vue-server-renderer'
  4. import VueSSRServerPlugin from '../../packages/vue-server-renderer/server-plugin'
  5. export function createRenderer (file, options, cb) {
  6. if (typeof options === 'function') {
  7. cb = options
  8. options = undefined
  9. }
  10. const asBundle = !!(options && options.asBundle)
  11. if (options) delete options.asBundle
  12. compileWithWebpack(file, {
  13. target: 'node',
  14. devtool: asBundle ? '#source-map' : false,
  15. output: {
  16. path: '/',
  17. filename: 'bundle.js',
  18. libraryTarget: 'commonjs2'
  19. },
  20. externals: [require.resolve('../../dist/vue.runtime.common.js')],
  21. plugins: asBundle
  22. ? [new VueSSRServerPlugin()]
  23. : []
  24. }, fs => {
  25. const bundle = asBundle
  26. ? JSON.parse(fs.readFileSync('/vue-ssr-server-bundle.json', 'utf-8'))
  27. : fs.readFileSync('/bundle.js', 'utf-8')
  28. const renderer = createBundleRenderer(bundle, options)
  29. cb(renderer)
  30. })
  31. }
  32. describe('SSR: bundle renderer', () => {
  33. createAssertions(true)
  34. createAssertions(false)
  35. })
  36. function createAssertions (runInNewContext) {
  37. it('renderToString', done => {
  38. createRenderer('app.js', { runInNewContext }, renderer => {
  39. const context = { url: '/test' }
  40. renderer.renderToString(context, (err, res) => {
  41. expect(err).toBeNull()
  42. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  43. expect(context.msg).toBe('hello')
  44. done()
  45. })
  46. })
  47. })
  48. it('renderToStream', done => {
  49. createRenderer('app.js', { runInNewContext }, renderer => {
  50. const context = { url: '/test' }
  51. const stream = renderer.renderToStream(context)
  52. let res = ''
  53. stream.on('data', chunk => {
  54. res += chunk.toString()
  55. })
  56. stream.on('end', () => {
  57. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  58. expect(context.msg).toBe('hello')
  59. done()
  60. })
  61. })
  62. })
  63. it('renderToString catch error', done => {
  64. createRenderer('error.js', { runInNewContext }, renderer => {
  65. renderer.renderToString(err => {
  66. expect(err.message).toBe('foo')
  67. done()
  68. })
  69. })
  70. })
  71. it('renderToString catch Promise rejection', done => {
  72. createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
  73. renderer.renderToString(err => {
  74. expect(err.message).toBe('foo')
  75. done()
  76. })
  77. })
  78. })
  79. it('renderToStream catch error', done => {
  80. createRenderer('error.js', { runInNewContext }, renderer => {
  81. const stream = renderer.renderToStream()
  82. stream.on('error', err => {
  83. expect(err.message).toBe('foo')
  84. done()
  85. })
  86. })
  87. })
  88. it('renderToStream catch Promise rejection', done => {
  89. createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
  90. const stream = renderer.renderToStream()
  91. stream.on('error', err => {
  92. expect(err.message).toBe('foo')
  93. done()
  94. })
  95. })
  96. })
  97. it('render with cache (get/set)', done => {
  98. const cache = {}
  99. const get = jasmine.createSpy('get')
  100. const set = jasmine.createSpy('set')
  101. const options = {
  102. runInNewContext,
  103. cache: {
  104. // async
  105. get: (key, cb) => {
  106. setTimeout(() => {
  107. get(key)
  108. cb(cache[key])
  109. }, 0)
  110. },
  111. set: (key, val) => {
  112. set(key, val)
  113. cache[key] = val
  114. }
  115. }
  116. }
  117. createRenderer('cache.js', options, renderer => {
  118. const expected = '<div data-server-rendered="true">/test</div>'
  119. const key = 'app::1'
  120. renderer.renderToString((err, res) => {
  121. expect(err).toBeNull()
  122. expect(res).toBe(expected)
  123. expect(get).toHaveBeenCalledWith(key)
  124. const setArgs = set.calls.argsFor(0)
  125. expect(setArgs[0]).toBe(key)
  126. expect(setArgs[1].html).toBe(expected)
  127. expect(cache[key].html).toBe(expected)
  128. renderer.renderToString((err, res) => {
  129. expect(err).toBeNull()
  130. expect(res).toBe(expected)
  131. expect(get.calls.count()).toBe(2)
  132. expect(set.calls.count()).toBe(1)
  133. done()
  134. })
  135. })
  136. })
  137. })
  138. it('render with cache (get/set/has)', done => {
  139. const cache = {}
  140. const has = jasmine.createSpy('has')
  141. const get = jasmine.createSpy('get')
  142. const set = jasmine.createSpy('set')
  143. const options = {
  144. runInNewContext,
  145. cache: {
  146. // async
  147. has: (key, cb) => {
  148. has(key)
  149. cb(!!cache[key])
  150. },
  151. // sync
  152. get: key => {
  153. get(key)
  154. return cache[key]
  155. },
  156. set: (key, val) => {
  157. set(key, val)
  158. cache[key] = val
  159. }
  160. }
  161. }
  162. createRenderer('cache.js', options, renderer => {
  163. const expected = '<div data-server-rendered="true">/test</div>'
  164. const key = 'app::1'
  165. renderer.renderToString((err, res) => {
  166. expect(err).toBeNull()
  167. expect(res).toBe(expected)
  168. expect(has).toHaveBeenCalledWith(key)
  169. expect(get).not.toHaveBeenCalled()
  170. const setArgs = set.calls.argsFor(0)
  171. expect(setArgs[0]).toBe(key)
  172. expect(setArgs[1].html).toBe(expected)
  173. expect(cache[key].html).toBe(expected)
  174. renderer.renderToString((err, res) => {
  175. expect(err).toBeNull()
  176. expect(res).toBe(expected)
  177. expect(has.calls.count()).toBe(2)
  178. expect(get.calls.count()).toBe(1)
  179. expect(set.calls.count()).toBe(1)
  180. done()
  181. })
  182. })
  183. })
  184. })
  185. it('render with cache (nested)', done => {
  186. const cache = new LRU({ maxAge: Infinity })
  187. spyOn(cache, 'get').and.callThrough()
  188. spyOn(cache, 'set').and.callThrough()
  189. const options = {
  190. cache,
  191. runInNewContext
  192. }
  193. createRenderer('nested-cache.js', options, renderer => {
  194. const expected = '<div data-server-rendered="true">/test</div>'
  195. const key = 'app::1'
  196. const context1 = { registered: [] }
  197. const context2 = { registered: [] }
  198. renderer.renderToString(context1, (err, res) => {
  199. expect(err).toBeNull()
  200. expect(res).toBe(expected)
  201. expect(cache.set.calls.count()).toBe(3) // 3 nested components cached
  202. const cached = cache.get(key)
  203. expect(cached.html).toBe(expected)
  204. expect(cache.get.calls.count()).toBe(1)
  205. // assert component usage registration for nested children
  206. expect(context1.registered).toEqual(['app', 'child', 'grandchild'])
  207. renderer.renderToString(context2, (err, res) => {
  208. expect(err).toBeNull()
  209. expect(res).toBe(expected)
  210. expect(cache.set.calls.count()).toBe(3) // no new cache sets
  211. expect(cache.get.calls.count()).toBe(2) // 1 get for root
  212. expect(context2.registered).toEqual(['app', 'child', 'grandchild'])
  213. done()
  214. })
  215. })
  216. })
  217. })
  218. it('render with cache (opt-out)', done => {
  219. const cache = {}
  220. const get = jasmine.createSpy('get')
  221. const set = jasmine.createSpy('set')
  222. const options = {
  223. runInNewContext,
  224. cache: {
  225. // async
  226. get: (key, cb) => {
  227. setTimeout(() => {
  228. get(key)
  229. cb(cache[key])
  230. }, 0)
  231. },
  232. set: (key, val) => {
  233. set(key, val)
  234. cache[key] = val
  235. }
  236. }
  237. }
  238. createRenderer('cache-opt-out.js', options, renderer => {
  239. const expected = '<div data-server-rendered="true">/test</div>'
  240. renderer.renderToString((err, res) => {
  241. expect(err).toBeNull()
  242. expect(res).toBe(expected)
  243. expect(get).not.toHaveBeenCalled()
  244. expect(set).not.toHaveBeenCalled()
  245. renderer.renderToString((err, res) => {
  246. expect(err).toBeNull()
  247. expect(res).toBe(expected)
  248. expect(get).not.toHaveBeenCalled()
  249. expect(set).not.toHaveBeenCalled()
  250. done()
  251. })
  252. })
  253. })
  254. })
  255. it('renderToString (bundle format with code split)', done => {
  256. createRenderer('split.js', { runInNewContext, asBundle: true }, renderer => {
  257. const context = { url: '/test' }
  258. renderer.renderToString(context, (err, res) => {
  259. expect(err).toBeNull()
  260. expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
  261. done()
  262. })
  263. })
  264. })
  265. it('renderToStream (bundle format with code split)', done => {
  266. createRenderer('split.js', { runInNewContext, asBundle: true }, renderer => {
  267. const context = { url: '/test' }
  268. const stream = renderer.renderToStream(context)
  269. let res = ''
  270. stream.on('data', chunk => {
  271. res += chunk.toString()
  272. })
  273. stream.on('end', () => {
  274. expect(res).toBe('<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>')
  275. done()
  276. })
  277. })
  278. })
  279. it('renderToString catch error (bundle format with source map)', done => {
  280. createRenderer('error.js', { runInNewContext, asBundle: true }, renderer => {
  281. renderer.renderToString(err => {
  282. expect(err.stack).toContain('test/ssr/fixtures/error.js:1:6')
  283. expect(err.message).toBe('foo')
  284. done()
  285. })
  286. })
  287. })
  288. it('renderToString catch error (bundle format with source map)', done => {
  289. createRenderer('error.js', { runInNewContext, asBundle: true }, renderer => {
  290. const stream = renderer.renderToStream()
  291. stream.on('error', err => {
  292. expect(err.stack).toContain('test/ssr/fixtures/error.js:1:6')
  293. expect(err.message).toBe('foo')
  294. done()
  295. })
  296. })
  297. })
  298. it('renderToString return Promise', done => {
  299. createRenderer('app.js', { runInNewContext }, renderer => {
  300. const context = { url: '/test' }
  301. renderer.renderToString(context).then(res => {
  302. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  303. expect(context.msg).toBe('hello')
  304. done()
  305. })
  306. })
  307. })
  308. it('renderToString return Promise (error)', done => {
  309. createRenderer('error.js', { runInNewContext }, renderer => {
  310. renderer.renderToString().catch(err => {
  311. expect(err.message).toBe('foo')
  312. done()
  313. })
  314. })
  315. })
  316. it('renderToString return Promise (Promise rejection)', done => {
  317. createRenderer('promise-rejection.js', { runInNewContext }, renderer => {
  318. renderer.renderToString().catch(err => {
  319. expect(err.message).toBe('foo')
  320. done()
  321. })
  322. })
  323. })
  324. }