@navios/adapter-xml 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +532 -0
  3. package/bun-plugin.mts +129 -0
  4. package/bunPlugin.cache +1 -0
  5. package/bunfig.toml +3 -0
  6. package/dist/bun-plugin.d.mts +4 -0
  7. package/dist/bun-plugin.d.mts.map +1 -0
  8. package/dist/e2e/bun/xml-stream.spec.d.ts +2 -0
  9. package/dist/e2e/bun/xml-stream.spec.d.ts.map +1 -0
  10. package/dist/e2e/fastify/xml-stream.spec.d.ts +2 -0
  11. package/dist/e2e/fastify/xml-stream.spec.d.ts.map +1 -0
  12. package/dist/src/adapters/index.d.mts +2 -0
  13. package/dist/src/adapters/index.d.mts.map +1 -0
  14. package/dist/src/adapters/xml-stream-adapter.service.d.mts +21 -0
  15. package/dist/src/adapters/xml-stream-adapter.service.d.mts.map +1 -0
  16. package/dist/src/decorators/component.decorator.d.mts +17 -0
  17. package/dist/src/decorators/component.decorator.d.mts.map +1 -0
  18. package/dist/src/decorators/component.decorator.spec.d.mts +2 -0
  19. package/dist/src/decorators/component.decorator.spec.d.mts.map +1 -0
  20. package/dist/src/decorators/index.d.mts +4 -0
  21. package/dist/src/decorators/index.d.mts.map +1 -0
  22. package/dist/src/decorators/xml-stream.decorator.d.mts +42 -0
  23. package/dist/src/decorators/xml-stream.decorator.d.mts.map +1 -0
  24. package/dist/src/define-environment.d.mts +31 -0
  25. package/dist/src/define-environment.d.mts.map +1 -0
  26. package/dist/src/handlers/index.d.mts +2 -0
  27. package/dist/src/handlers/index.d.mts.map +1 -0
  28. package/dist/src/handlers/xml-stream.d.mts +23 -0
  29. package/dist/src/handlers/xml-stream.d.mts.map +1 -0
  30. package/dist/src/index.d.mts +12 -0
  31. package/dist/src/index.d.mts.map +1 -0
  32. package/dist/src/jsx-dev-runtime.d.mts +5 -0
  33. package/dist/src/jsx-dev-runtime.d.mts.map +1 -0
  34. package/dist/src/jsx-runtime.d.mts +3 -0
  35. package/dist/src/jsx-runtime.d.mts.map +1 -0
  36. package/dist/src/jsx.d.mts +18 -0
  37. package/dist/src/jsx.d.mts.map +1 -0
  38. package/dist/src/runtime/create-element.d.mts +25 -0
  39. package/dist/src/runtime/create-element.d.mts.map +1 -0
  40. package/dist/src/runtime/fragment.d.mts +2 -0
  41. package/dist/src/runtime/fragment.d.mts.map +1 -0
  42. package/dist/src/runtime/index.d.mts +5 -0
  43. package/dist/src/runtime/index.d.mts.map +1 -0
  44. package/dist/src/runtime/render-to-xml.d.mts +20 -0
  45. package/dist/src/runtime/render-to-xml.d.mts.map +1 -0
  46. package/dist/src/runtime/render-to-xml.spec.d.mts +2 -0
  47. package/dist/src/runtime/render-to-xml.spec.d.mts.map +1 -0
  48. package/dist/src/runtime/special-nodes.d.mts +24 -0
  49. package/dist/src/runtime/special-nodes.d.mts.map +1 -0
  50. package/dist/src/tags/define-tag.d.mts +33 -0
  51. package/dist/src/tags/define-tag.d.mts.map +1 -0
  52. package/dist/src/tags/define-tag.spec.d.mts +2 -0
  53. package/dist/src/tags/define-tag.spec.d.mts.map +1 -0
  54. package/dist/src/tags/index.d.mts +3 -0
  55. package/dist/src/tags/index.d.mts.map +1 -0
  56. package/dist/src/types/component.d.mts +15 -0
  57. package/dist/src/types/component.d.mts.map +1 -0
  58. package/dist/src/types/config.d.mts +10 -0
  59. package/dist/src/types/config.d.mts.map +1 -0
  60. package/dist/src/types/index.d.mts +5 -0
  61. package/dist/src/types/index.d.mts.map +1 -0
  62. package/dist/src/types/xml-node.d.mts +35 -0
  63. package/dist/src/types/xml-node.d.mts.map +1 -0
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  65. package/dist/tsconfig.spec.tsbuildinfo +1 -0
  66. package/dist/tsconfig.tsbuildinfo +1 -0
  67. package/dist/tsup.config.d.mts +3 -0
  68. package/dist/tsup.config.d.mts.map +1 -0
  69. package/dist/vitest.config.d.mts +3 -0
  70. package/dist/vitest.config.d.mts.map +1 -0
  71. package/dist/vitest.e2e.fastify.config.d.mts +3 -0
  72. package/dist/vitest.e2e.fastify.config.d.mts.map +1 -0
  73. package/e2e/bun/xml-stream.spec.tsx +553 -0
  74. package/e2e/fastify/xml-stream.spec.tsx +569 -0
  75. package/jsx.d.ts +42 -0
  76. package/lib/_tsup-dts-rollup.d.mts +414 -0
  77. package/lib/_tsup-dts-rollup.d.ts +414 -0
  78. package/lib/chunk-6OR6LGJA.mjs +153 -0
  79. package/lib/chunk-6OR6LGJA.mjs.map +1 -0
  80. package/lib/index.d.mts +29 -0
  81. package/lib/index.d.ts +29 -0
  82. package/lib/index.js +376 -0
  83. package/lib/index.js.map +1 -0
  84. package/lib/index.mjs +256 -0
  85. package/lib/index.mjs.map +1 -0
  86. package/lib/jsx-dev-runtime.d.mts +4 -0
  87. package/lib/jsx-dev-runtime.d.ts +4 -0
  88. package/lib/jsx-dev-runtime.js +61 -0
  89. package/lib/jsx-dev-runtime.js.map +1 -0
  90. package/lib/jsx-dev-runtime.mjs +9 -0
  91. package/lib/jsx-dev-runtime.mjs.map +1 -0
  92. package/lib/jsx-runtime.d.mts +3 -0
  93. package/lib/jsx-runtime.d.ts +3 -0
  94. package/lib/jsx-runtime.js +57 -0
  95. package/lib/jsx-runtime.js.map +1 -0
  96. package/lib/jsx-runtime.mjs +3 -0
  97. package/lib/jsx-runtime.mjs.map +1 -0
  98. package/lib/jsx.d.mts +1 -0
  99. package/lib/jsx.d.ts +1 -0
  100. package/lib/jsx.js +4 -0
  101. package/lib/jsx.js.map +1 -0
  102. package/lib/jsx.mjs +3 -0
  103. package/lib/jsx.mjs.map +1 -0
  104. package/package.json +80 -0
  105. package/project.json +91 -0
  106. package/src/adapters/index.mts +1 -0
  107. package/src/adapters/xml-stream-adapter.service.mts +121 -0
  108. package/src/decorators/component.decorator.mts +102 -0
  109. package/src/decorators/component.decorator.spec.mts +345 -0
  110. package/src/decorators/index.mts +4 -0
  111. package/src/decorators/xml-stream.decorator.mts +93 -0
  112. package/src/define-environment.mts +40 -0
  113. package/src/handlers/index.mts +1 -0
  114. package/src/handlers/xml-stream.mts +31 -0
  115. package/src/index.mts +41 -0
  116. package/src/jsx-dev-runtime.mts +8 -0
  117. package/src/jsx-runtime.mts +2 -0
  118. package/src/jsx.mts +25 -0
  119. package/src/runtime/create-element.mts +113 -0
  120. package/src/runtime/fragment.mts +1 -0
  121. package/src/runtime/index.mts +4 -0
  122. package/src/runtime/render-to-xml.mts +214 -0
  123. package/src/runtime/render-to-xml.spec.mts +360 -0
  124. package/src/runtime/special-nodes.mts +32 -0
  125. package/src/tags/define-tag.mts +54 -0
  126. package/src/tags/define-tag.spec.mts +250 -0
  127. package/src/tags/index.mts +2 -0
  128. package/src/types/component.mts +16 -0
  129. package/src/types/config.mts +15 -0
  130. package/src/types/index.mts +23 -0
  131. package/src/types/jsx.d.ts +21 -0
  132. package/src/types/xml-node.mts +50 -0
  133. package/tsconfig.json +24 -0
  134. package/tsconfig.lib.json +8 -0
  135. package/tsconfig.spec.json +25 -0
  136. package/tsup.config.mts +18 -0
  137. package/vitest.config.mts +9 -0
  138. package/vitest.e2e.fastify.config.mts +29 -0
@@ -0,0 +1,553 @@
1
+ import type { XmlComponent, XmlStreamParams } from '../../src/index.mjs'
2
+
3
+ import { builder } from '@navios/builder'
4
+ import {
5
+ Controller,
6
+ Endpoint,
7
+ inject,
8
+ Injectable,
9
+ InjectableScope,
10
+ Module,
11
+ NaviosApplication,
12
+ NaviosFactory,
13
+ type EndpointParams,
14
+ } from '@navios/core'
15
+ import { defineBunEnvironment } from '@navios/adapter-bun'
16
+
17
+ import supertest from 'supertest'
18
+ import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
19
+ import { z } from 'zod/v4'
20
+
21
+ import {
22
+ CData,
23
+ Component,
24
+ DangerouslyInsertRawXml,
25
+ declareXmlStream,
26
+ defineXmlEnvironment,
27
+ Fragment,
28
+ XmlStream,
29
+ createElement,
30
+ } from '../../src/index.mjs'
31
+
32
+ // Helper to merge environments
33
+ function mergeEnvironments(...envs: Array<{ httpTokens: Map<any, any> }>): {
34
+ httpTokens: Map<any, any>
35
+ } {
36
+ const merged = new Map()
37
+ for (const env of envs) {
38
+ for (const [key, value] of env.httpTokens) {
39
+ merged.set(key, value)
40
+ }
41
+ }
42
+ return { httpTokens: merged }
43
+ }
44
+
45
+ describe('XML Stream with Bun adapter', () => {
46
+ let server: NaviosApplication
47
+ let serverUrl: string
48
+
49
+ // Request scoped service for tracking
50
+ @Injectable({
51
+ scope: InjectableScope.Request,
52
+ })
53
+ class RequestTrackerService {
54
+ private requestId: string = Math.random().toString(36).substring(7)
55
+
56
+ getRequestId(): string {
57
+ return this.requestId
58
+ }
59
+ }
60
+
61
+ // Injectable data service
62
+ @Injectable()
63
+ class DataService {
64
+ getItems() {
65
+ return [
66
+ { id: '1', title: 'First Item', description: 'Description 1' },
67
+ { id: '2', title: 'Second Item', description: 'Description 2' },
68
+ { id: '3', title: 'Third Item', description: 'Description 3' },
69
+ ]
70
+ }
71
+
72
+ getItem(id: string) {
73
+ return this.getItems().find((item) => item.id === id)
74
+ }
75
+ }
76
+
77
+ // Class component with DI
78
+ @Component()
79
+ class ItemComponent implements XmlComponent {
80
+ private dataService = inject(DataService)
81
+ private tracker = inject(RequestTrackerService)
82
+
83
+ constructor(private props: { id: string }) {}
84
+
85
+ render() {
86
+ const item = this.dataService.getItem(this.props.id)
87
+ if (!item) {
88
+ return createElement('error', null, 'Item not found')
89
+ }
90
+ return createElement(
91
+ 'item',
92
+ { id: item.id, requestId: this.tracker.getRequestId() },
93
+ createElement('title', null, item.title),
94
+ createElement('description', null, item.description),
95
+ )
96
+ }
97
+ }
98
+
99
+ // Async component
100
+ async function AsyncDataComponent({ delay }: { delay: number }) {
101
+ await new Promise((resolve) => setTimeout(resolve, delay))
102
+ return createElement('async-data', null, `Loaded after ${delay}ms`)
103
+ }
104
+
105
+ // Simple RSS feed endpoint
106
+ const getRssFeed = declareXmlStream({
107
+ method: 'GET',
108
+ url: '/feed.xml',
109
+ querySchema: undefined,
110
+ requestSchema: undefined,
111
+ contentType: 'application/rss+xml',
112
+ xmlDeclaration: true,
113
+ })
114
+
115
+ // Atom feed with query params
116
+ const getAtomFeed = declareXmlStream({
117
+ method: 'GET',
118
+ url: '/atom.xml',
119
+ querySchema: z.object({
120
+ limit: z.coerce.number().optional().default(10),
121
+ }),
122
+ requestSchema: undefined,
123
+ contentType: 'application/atom+xml',
124
+ xmlDeclaration: true,
125
+ })
126
+
127
+ // Sitemap endpoint
128
+ const getSitemap = declareXmlStream({
129
+ method: 'GET',
130
+ url: '/sitemap.xml',
131
+ querySchema: undefined,
132
+ requestSchema: undefined,
133
+ contentType: 'application/xml',
134
+ xmlDeclaration: true,
135
+ })
136
+
137
+ // Dynamic XML with URL params (use path without extension for Bun compatibility)
138
+ const getItemXml = declareXmlStream({
139
+ method: 'GET',
140
+ url: '/items/$id',
141
+ querySchema: undefined,
142
+ requestSchema: undefined,
143
+ contentType: 'application/xml',
144
+ xmlDeclaration: true,
145
+ })
146
+
147
+ // POST endpoint for XML generation
148
+ const postGenerateXml = declareXmlStream({
149
+ method: 'POST',
150
+ url: '/generate.xml',
151
+ querySchema: undefined,
152
+ requestSchema: z.object({
153
+ title: z.string(),
154
+ items: z.array(z.string()),
155
+ }),
156
+ contentType: 'application/xml',
157
+ xmlDeclaration: true,
158
+ })
159
+
160
+ // Endpoint with async components
161
+ const getAsyncXml = declareXmlStream({
162
+ method: 'GET',
163
+ url: '/async.xml',
164
+ querySchema: undefined,
165
+ requestSchema: undefined,
166
+ contentType: 'application/xml',
167
+ xmlDeclaration: true,
168
+ })
169
+
170
+ // Endpoint with class components (DI) - use path without extension for Bun compatibility
171
+ const getDiXml = declareXmlStream({
172
+ method: 'GET',
173
+ url: '/di/$id',
174
+ querySchema: undefined,
175
+ requestSchema: undefined,
176
+ contentType: 'application/xml',
177
+ xmlDeclaration: true,
178
+ })
179
+
180
+ // Endpoint with CDATA and raw XML
181
+ const getSpecialXml = declareXmlStream({
182
+ method: 'GET',
183
+ url: '/special.xml',
184
+ querySchema: undefined,
185
+ requestSchema: undefined,
186
+ contentType: 'application/xml',
187
+ xmlDeclaration: true,
188
+ })
189
+
190
+ // Also test regular JSON endpoint alongside XML
191
+ const getJsonEndpoint = builder().declareEndpoint({
192
+ method: 'GET',
193
+ url: '/api/data',
194
+ responseSchema: z.object({
195
+ message: z.string(),
196
+ }),
197
+ })
198
+
199
+ @Controller()
200
+ class FeedController {
201
+ private dataService = inject(DataService)
202
+ private tracker = inject(RequestTrackerService)
203
+
204
+ @XmlStream(getRssFeed)
205
+ async getRssFeed(_params: XmlStreamParams<typeof getRssFeed>) {
206
+ const items = this.dataService.getItems()
207
+ return (
208
+ <rss version="2.0">
209
+ <channel>
210
+ <title>My Blog</title>
211
+ <link>https://example.com</link>
212
+ <description>A sample RSS feed</description>
213
+ {items.map((item) => (
214
+ <item key={item.id}>
215
+ <title>{item.title}</title>
216
+ <link>https://example.com/items/{item.id}</link>
217
+ <description>{item.description}</description>
218
+ </item>
219
+ ))}
220
+ </channel>
221
+ </rss>
222
+ )
223
+ }
224
+
225
+ @XmlStream(getAtomFeed)
226
+ async getAtomFeed(params: XmlStreamParams<typeof getAtomFeed>) {
227
+ const limit = params.params.limit
228
+ const items = this.dataService.getItems().slice(0, limit)
229
+ return (
230
+ <feed xmlns="http://www.w3.org/2005/Atom">
231
+ <title>My Atom Feed</title>
232
+ <link href="https://example.com/atom.xml" rel="self" />
233
+ <id>https://example.com/</id>
234
+ <updated>2025-01-01T00:00:00Z</updated>
235
+ {items.map((item) => (
236
+ <entry key={item.id}>
237
+ <title>{item.title}</title>
238
+ <id>https://example.com/items/{item.id}</id>
239
+ <updated>2025-01-01T00:00:00Z</updated>
240
+ <summary>{item.description}</summary>
241
+ </entry>
242
+ ))}
243
+ </feed>
244
+ )
245
+ }
246
+
247
+ @XmlStream(getSitemap)
248
+ async getSitemap(_params: XmlStreamParams<typeof getSitemap>) {
249
+ const items = this.dataService.getItems()
250
+ return (
251
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
252
+ <url>
253
+ <loc>https://example.com/</loc>
254
+ <lastmod>2025-01-01</lastmod>
255
+ <changefreq>daily</changefreq>
256
+ <priority>1.0</priority>
257
+ </url>
258
+ {items.map((item) => (
259
+ <url key={item.id}>
260
+ <loc>https://example.com/items/{item.id}</loc>
261
+ <lastmod>2025-01-01</lastmod>
262
+ <changefreq>weekly</changefreq>
263
+ <priority>0.8</priority>
264
+ </url>
265
+ ))}
266
+ </urlset>
267
+ )
268
+ }
269
+
270
+ @XmlStream(getItemXml)
271
+ async getItemXml(params: XmlStreamParams<typeof getItemXml>) {
272
+ const item = this.dataService.getItem(params.urlParams.id as string)
273
+ if (!item) {
274
+ return (
275
+ <error>
276
+ <message>Item not found</message>
277
+ <requestedId>{params.urlParams.id}</requestedId>
278
+ </error>
279
+ )
280
+ }
281
+ return (
282
+ <item id={item.id}>
283
+ <title>{item.title}</title>
284
+ <description>{item.description}</description>
285
+ </item>
286
+ )
287
+ }
288
+
289
+ @XmlStream(postGenerateXml)
290
+ async postGenerateXml(params: XmlStreamParams<typeof postGenerateXml>) {
291
+ const { title, items } = params.data
292
+ return (
293
+ <generated>
294
+ <title>{title}</title>
295
+ <items count={String(items.length)}>
296
+ {items.map((item, index) => (
297
+ <item index={String(index)} key={index}>
298
+ {item}
299
+ </item>
300
+ ))}
301
+ </items>
302
+ </generated>
303
+ )
304
+ }
305
+
306
+ @XmlStream(getAsyncXml)
307
+ async getAsyncXml(_params: XmlStreamParams<typeof getAsyncXml>) {
308
+ return (
309
+ <root>
310
+ <sync>Immediate content</sync>
311
+ <AsyncDataComponent delay={10} />
312
+ <AsyncDataComponent delay={5} />
313
+ </root>
314
+ )
315
+ }
316
+
317
+ @XmlStream(getDiXml)
318
+ async getDiXml(params: XmlStreamParams<typeof getDiXml>) {
319
+ return (
320
+ <root requestId={this.tracker.getRequestId()}>
321
+ <ItemComponent id={params.urlParams.id as string} />
322
+ </root>
323
+ )
324
+ }
325
+
326
+ @XmlStream(getSpecialXml)
327
+ async getSpecialXml(_params: XmlStreamParams<typeof getSpecialXml>) {
328
+ return (
329
+ <root>
330
+ <cdata-example>
331
+ <CData>
332
+ {'This is <raw> HTML & XML content that should not be escaped'}
333
+ </CData>
334
+ </cdata-example>
335
+ <raw-xml-example>
336
+ <DangerouslyInsertRawXml>
337
+ {'<nested><element attr="value">text</element></nested>'}
338
+ </DangerouslyInsertRawXml>
339
+ </raw-xml-example>
340
+ <escaped>{'<this> & "that" should be escaped'}</escaped>
341
+ </root>
342
+ )
343
+ }
344
+
345
+ @Endpoint(getJsonEndpoint)
346
+ async getJsonData(_params: EndpointParams<typeof getJsonEndpoint>) {
347
+ return { message: 'JSON endpoint works alongside XML' }
348
+ }
349
+ }
350
+
351
+ @Module({
352
+ controllers: [FeedController],
353
+ })
354
+ class AppModule {}
355
+
356
+ beforeAll(async () => {
357
+ const bunEnv = defineBunEnvironment()
358
+ const xmlEnv = defineXmlEnvironment()
359
+ const mergedEnv = mergeEnvironments(bunEnv, xmlEnv)
360
+
361
+ server = await NaviosFactory.create(AppModule, {
362
+ adapter: mergedEnv,
363
+ })
364
+ await server.init()
365
+ await server.listen({ port: 3002, host: 'localhost' })
366
+ serverUrl = server.getServer().url.href
367
+ })
368
+
369
+ afterAll(async () => {
370
+ await server.close()
371
+ })
372
+
373
+ describe('Basic XML endpoints', () => {
374
+ it('should return RSS feed with correct content type', async () => {
375
+ const response = await supertest(serverUrl).get('/feed.xml')
376
+
377
+ expect(response.status).toBe(200)
378
+ expect(response.headers['content-type']).toContain('application/rss+xml')
379
+ expect(response.text).toContain('<?xml version="1.0" encoding="UTF-8"?>')
380
+ expect(response.text).toContain('<rss version="2.0">')
381
+ expect(response.text).toContain('<title>My Blog</title>')
382
+ expect(response.text).toContain('<item>')
383
+ expect(response.text).toContain('<title>First Item</title>')
384
+ })
385
+
386
+ it('should return Atom feed with query params', async () => {
387
+ const response = await supertest(serverUrl).get('/atom.xml?limit=2')
388
+
389
+ expect(response.status).toBe(200)
390
+ expect(response.headers['content-type']).toContain('application/atom+xml')
391
+ expect(response.text).toContain(
392
+ '<feed xmlns="http://www.w3.org/2005/Atom">',
393
+ )
394
+
395
+ // Count entries - should be limited to 2
396
+ const entryCount = (response.text.match(/<entry>/g) || []).length
397
+ expect(entryCount).toBe(2)
398
+ })
399
+
400
+ it('should return sitemap XML', async () => {
401
+ const response = await supertest(serverUrl).get('/sitemap.xml')
402
+
403
+ expect(response.status).toBe(200)
404
+ expect(response.headers['content-type']).toContain('application/xml')
405
+ expect(response.text).toContain(
406
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
407
+ )
408
+ expect(response.text).toContain('<loc>https://example.com/</loc>')
409
+ expect(response.text).toContain('<priority>1.0</priority>')
410
+ })
411
+ })
412
+
413
+ describe('URL parameters', () => {
414
+ it('should handle URL parameters in XML endpoint', async () => {
415
+ const response = await supertest(serverUrl).get('/items/1')
416
+
417
+ expect(response.status).toBe(200)
418
+ expect(response.text).toContain('<item id="1">')
419
+ expect(response.text).toContain('<title>First Item</title>')
420
+ })
421
+
422
+ it('should handle non-existent item', async () => {
423
+ const response = await supertest(serverUrl).get('/items/999')
424
+
425
+ expect(response.status).toBe(200)
426
+ expect(response.text).toContain('<error>')
427
+ expect(response.text).toContain('<message>Item not found</message>')
428
+ expect(response.text).toContain('<requestedId>999</requestedId>')
429
+ })
430
+ })
431
+
432
+ describe('POST with request body', () => {
433
+ it('should generate XML from POST body', async () => {
434
+ const response = await supertest(serverUrl)
435
+ .post('/generate.xml')
436
+ .send({
437
+ title: 'Generated Document',
438
+ items: ['Alpha', 'Beta', 'Gamma'],
439
+ })
440
+
441
+ expect(response.status).toBe(200)
442
+ expect(response.text).toContain('<generated>')
443
+ expect(response.text).toContain('<title>Generated Document</title>')
444
+ expect(response.text).toContain('<items count="3">')
445
+ expect(response.text).toContain('<item index="0">Alpha</item>')
446
+ expect(response.text).toContain('<item index="1">Beta</item>')
447
+ expect(response.text).toContain('<item index="2">Gamma</item>')
448
+ })
449
+ })
450
+
451
+ describe('Async components', () => {
452
+ it('should resolve async components in XML', async () => {
453
+ const response = await supertest(serverUrl).get('/async.xml')
454
+
455
+ expect(response.status).toBe(200)
456
+ expect(response.text).toContain('<sync>Immediate content</sync>')
457
+ expect(response.text).toContain(
458
+ '<async-data>Loaded after 10ms</async-data>',
459
+ )
460
+ expect(response.text).toContain(
461
+ '<async-data>Loaded after 5ms</async-data>',
462
+ )
463
+ })
464
+ })
465
+
466
+ describe('Class components with DI', () => {
467
+ it('should render class components with injected services', async () => {
468
+ const response = await supertest(serverUrl).get('/di/1')
469
+
470
+ expect(response.status).toBe(200)
471
+ expect(response.text).toContain('<root requestId=')
472
+ expect(response.text).toContain('<item id="1"')
473
+ expect(response.text).toContain('<title>First Item</title>')
474
+ })
475
+
476
+ it('should isolate request-scoped services across parallel requests', async () => {
477
+ const requests = [
478
+ supertest(serverUrl).get('/di/1'),
479
+ supertest(serverUrl).get('/di/2'),
480
+ supertest(serverUrl).get('/di/3'),
481
+ ]
482
+
483
+ const responses = await Promise.all(requests)
484
+
485
+ // All should succeed
486
+ responses.forEach((response) => {
487
+ expect(response.status).toBe(200)
488
+ })
489
+
490
+ // Extract request IDs from the responses
491
+ const requestIds = responses.map((response) => {
492
+ const match = response.text.match(/requestId="([^"]+)"/)
493
+ return match ? match[1] : null
494
+ })
495
+
496
+ // All request IDs should be unique
497
+ const uniqueIds = new Set(requestIds.filter(Boolean))
498
+ expect(uniqueIds.size).toBe(3)
499
+ })
500
+ })
501
+
502
+ describe('Special XML content', () => {
503
+ it('should handle CDATA and raw XML correctly', async () => {
504
+ const response = await supertest(serverUrl).get('/special.xml')
505
+
506
+ expect(response.status).toBe(200)
507
+
508
+ // CDATA should preserve raw content
509
+ expect(response.text).toContain(
510
+ '<![CDATA[This is <raw> HTML & XML content that should not be escaped]]>',
511
+ )
512
+
513
+ // Raw XML should be inserted without escaping
514
+ expect(response.text).toContain(
515
+ '<nested><element attr="value">text</element></nested>',
516
+ )
517
+
518
+ // Regular content should be escaped
519
+ expect(response.text).toContain(
520
+ '&lt;this&gt; &amp; "that" should be escaped',
521
+ )
522
+ })
523
+ })
524
+
525
+ describe('Mixed endpoints', () => {
526
+ it('should work alongside regular JSON endpoints', async () => {
527
+ const xmlResponse = await supertest(serverUrl).get('/feed.xml')
528
+ const jsonResponse = await supertest(serverUrl).get('/api/data')
529
+
530
+ expect(xmlResponse.status).toBe(200)
531
+ expect(xmlResponse.headers['content-type']).toContain(
532
+ 'application/rss+xml',
533
+ )
534
+
535
+ expect(jsonResponse.status).toBe(200)
536
+ expect(jsonResponse.body.message).toBe(
537
+ 'JSON endpoint works alongside XML',
538
+ )
539
+ })
540
+ })
541
+
542
+ describe('Fragment support', () => {
543
+ it('should correctly render fragments within XML', async () => {
544
+ // The RSS feed uses fragments implicitly with array mapping
545
+ const response = await supertest(serverUrl).get('/feed.xml')
546
+
547
+ expect(response.status).toBe(200)
548
+ // Multiple items should be rendered without extra wrapper
549
+ const itemCount = (response.text.match(/<item>/g) || []).length
550
+ expect(itemCount).toBeGreaterThan(1)
551
+ })
552
+ })
553
+ })