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