ssr-bundle-render.spec.ts 9.2 KB

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