@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
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Oleksandr Hanzha
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,532 @@
1
+ # @navios/adapter-xml
2
+
3
+ A JSX-based XML adapter for Navios that enables building XML responses (RSS feeds, sitemaps, Atom feeds, etc.) using familiar JSX syntax with full TypeScript support.
4
+
5
+ ## Features
6
+
7
+ - **JSX Syntax** - Write XML using JSX with TypeScript type checking
8
+ - **Async Components** - Support for async components that fetch data during rendering
9
+ - **Class Components** - `@Component` decorator with dependency injection support via `@navios/di`
10
+ - **Type-Safe Tags** - Define custom XML tags with Zod schema validation
11
+ - **Runtime Agnostic** - Works with both Fastify and Bun adapters
12
+ - **CDATA Support** - Built-in `CData` component for safe text content
13
+ - **Raw XML** - `DangerouslyInsertRawXml` for pre-rendered content
14
+ - **XML Namespaces** - Full support for namespaced tags (e.g., `atom:link`)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @navios/adapter-xml
20
+ # or
21
+ yarn add @navios/adapter-xml
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ ### TypeScript Setup
27
+
28
+ Configure your `tsconfig.json` to use the JSX runtime:
29
+
30
+ ```json
31
+ {
32
+ "compilerOptions": {
33
+ "jsx": "react-jsx",
34
+ "jsxImportSource": "@navios/adapter-xml"
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Environment Setup
40
+
41
+ Merge the XML environment with your base adapter (Fastify or Bun):
42
+
43
+ ```typescript
44
+ import { defineFastifyEnvironment } from '@navios/adapter-fastify'
45
+ import { defineXmlEnvironment } from '@navios/adapter-xml'
46
+ import { NaviosFactory } from '@navios/core'
47
+
48
+ import { AppModule } from './app.module.mjs'
49
+
50
+ async function bootstrap() {
51
+ const fastifyEnv = defineFastifyEnvironment()
52
+ const xmlEnv = defineXmlEnvironment()
53
+
54
+ const mergedEnv = {
55
+ httpTokens: new Map([...fastifyEnv.httpTokens, ...xmlEnv.httpTokens]),
56
+ }
57
+
58
+ const app = await NaviosFactory.create(AppModule, {
59
+ adapter: mergedEnv,
60
+ })
61
+
62
+ await app.init()
63
+ await app.listen({ port: 3000 })
64
+ }
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ### Basic Example - RSS Feed
70
+
71
+ Define your XML tags:
72
+
73
+ ```typescript
74
+ // tags.ts
75
+ import { defineTag } from '@navios/adapter-xml'
76
+
77
+ import { z } from 'zod/v4'
78
+
79
+ export const rss = defineTag(
80
+ 'rss',
81
+ z.object({
82
+ version: z.literal('2.0'),
83
+ 'xmlns:atom': z.string().optional(),
84
+ }),
85
+ )
86
+ export const channel = defineTag('channel')
87
+ export const title = defineTag('title')
88
+ export const link = defineTag('link')
89
+ export const description = defineTag('description')
90
+ export const item = defineTag('item')
91
+ export const pubDate = defineTag('pubDate')
92
+ export const atomLink = defineTag(
93
+ 'atom:link',
94
+ z.object({
95
+ href: z.string(),
96
+ rel: z.string(),
97
+ type: z.string().optional(),
98
+ }),
99
+ )
100
+ ```
101
+
102
+ Declare your endpoint:
103
+
104
+ ```typescript
105
+ // api.ts
106
+ import { declareXmlStream } from '@navios/adapter-xml'
107
+
108
+ export const getRssFeed = declareXmlStream({
109
+ method: 'GET',
110
+ url: '/feed.xml',
111
+ contentType: 'application/rss+xml',
112
+ })
113
+ ```
114
+
115
+ Create the controller:
116
+
117
+ ```tsx
118
+ // feed.controller.tsx
119
+ import { Controller } from '@navios/core'
120
+ import { XmlStream } from '@navios/adapter-xml'
121
+ import { getRssFeed } from './api'
122
+ import {
123
+ rss,
124
+ channel,
125
+ title,
126
+ link,
127
+ description,
128
+ item,
129
+ pubDate,
130
+ atomLink,
131
+ } from './tags'
132
+
133
+ @Controller('/api')
134
+ export class FeedController {
135
+ @XmlStream(getRssFeed)
136
+ async getFeed() {
137
+ const posts = await this.fetchPosts()
138
+
139
+ return (
140
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
141
+ <channel>
142
+ <title>My Blog</title>
143
+ <link>https://example.com</link>
144
+ <description>Latest posts from my blog</description>
145
+ <atomLink
146
+ href="https://example.com/feed.xml"
147
+ rel="self"
148
+ type="application/rss+xml"
149
+ />
150
+ {posts.map((post) => (
151
+ <item>
152
+ <title>{post.title}</title>
153
+ <link>{post.url}</link>
154
+ <pubDate>{post.publishedAt.toUTCString()}</pubDate>
155
+ <description>{post.excerpt}</description>
156
+ </item>
157
+ ))}
158
+ </channel>
159
+ </rss>
160
+ )
161
+ }
162
+
163
+ private async fetchPosts() {
164
+ // Fetch posts from database
165
+ return []
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### Defining Tags
171
+
172
+ Use `defineTag` to create type-safe XML tags:
173
+
174
+ ```typescript
175
+ import { defineTag } from '@navios/adapter-xml'
176
+ import { z } from 'zod/v4'
177
+
178
+ // Simple tag without props validation
179
+ const item = defineTag('item')
180
+
181
+ // Tag with required props
182
+ const link = defineTag('link', z.object({
183
+ href: z.string().url(),
184
+ rel: z.enum(['self', 'alternate', 'enclosure']),
185
+ }))
186
+
187
+ // Namespaced tag
188
+ const atomLink = defineTag('atom:link', z.object({
189
+ href: z.string(),
190
+ rel: z.string(),
191
+ type: z.string().optional(),
192
+ }))
193
+
194
+ // Usage
195
+ <link href="https://example.com" rel="self" />
196
+ <atomLink href="https://example.com/feed" rel="alternate" />
197
+ ```
198
+
199
+ ### Async Components
200
+
201
+ Components can be async functions that fetch data during rendering:
202
+
203
+ ```tsx
204
+ interface PostItemProps {
205
+ postId: string
206
+ }
207
+
208
+ async function PostItem({ postId }: PostItemProps) {
209
+ const post = await fetchPostById(postId)
210
+
211
+ return (
212
+ <item>
213
+ <title>{post.title}</title>
214
+ <link>{post.url}</link>
215
+ <pubDate>{post.publishedAt.toUTCString()}</pubDate>
216
+ </item>
217
+ )
218
+ }
219
+
220
+ // Multiple async components are resolved in parallel
221
+ async function LatestPosts() {
222
+ const postIds = await getLatestPostIds()
223
+
224
+ return (
225
+ <>
226
+ {postIds.map((id) => (
227
+ <PostItem postId={id} />
228
+ ))}
229
+ </>
230
+ )
231
+ }
232
+ ```
233
+
234
+ ### Class Components
235
+
236
+ Class components use the `@Component` decorator and implement the `XmlComponent` interface. They support dependency injection via `@navios/di`, making them ideal for components that need access to services.
237
+
238
+ ```tsx
239
+ import { Component, XmlComponent } from '@navios/adapter-xml'
240
+ import { inject, Injectable } from '@navios/di'
241
+
242
+ // Define a service
243
+ @Injectable()
244
+ class PostService {
245
+ async getLatestPosts() {
246
+ // Fetch posts from database
247
+ return [{ title: 'Hello World', url: '/posts/hello' }]
248
+ }
249
+ }
250
+
251
+ // Basic class component without props
252
+ @Component()
253
+ class LatestPostsComponent implements XmlComponent {
254
+ private readonly postService = inject(PostService)
255
+
256
+ async render() {
257
+ const posts = await this.postService.getLatestPosts()
258
+
259
+ return (
260
+ <>
261
+ {posts.map((post) => (
262
+ <item>
263
+ <title>{post.title}</title>
264
+ <link>{post.url}</link>
265
+ </item>
266
+ ))}
267
+ </>
268
+ )
269
+ }
270
+ }
271
+
272
+ // Usage in JSX
273
+ <channel>
274
+ <LatestPostsComponent />
275
+ </channel>
276
+ ```
277
+
278
+ #### Class Components with Props
279
+
280
+ Use a Zod schema to define typed props for your component:
281
+
282
+ ```tsx
283
+ import { Component, XmlComponent, CData } from '@navios/adapter-xml'
284
+ import { z } from 'zod/v4'
285
+
286
+ const DescriptionSchema = z.object({
287
+ content: z.string(),
288
+ wrapInCData: z.boolean().optional(),
289
+ })
290
+
291
+ @Component({ schema: DescriptionSchema })
292
+ class DescriptionComponent implements XmlComponent {
293
+ constructor(private props: z.output<typeof DescriptionSchema>) {}
294
+
295
+ async render() {
296
+ const { content, wrapInCData } = this.props
297
+
298
+ return (
299
+ <description>
300
+ {wrapInCData ? <CData>{content}</CData> : content}
301
+ </description>
302
+ )
303
+ }
304
+ }
305
+
306
+ // Usage with typed props
307
+ <DescriptionComponent
308
+ content="<p>HTML content here</p>"
309
+ wrapInCData={true}
310
+ />
311
+ ```
312
+
313
+ #### Rendering with Container
314
+
315
+ When using class components, pass a DI container to `renderToXml`:
316
+
317
+ ```tsx
318
+ import { renderToXml } from '@navios/adapter-xml'
319
+ import { Container } from '@navios/di'
320
+
321
+ const container = new Container()
322
+ container.beginRequest('request-id')
323
+
324
+ const xml = await renderToXml(<RssFeed />, { container })
325
+ ```
326
+
327
+ ### CDATA Sections
328
+
329
+ Use `CData` for text content that may contain special characters:
330
+
331
+ ```tsx
332
+ import { CData } from '@navios/adapter-xml'
333
+ ;<description>
334
+ <CData>{`This content has <special> characters & more`}</CData>
335
+ </description>
336
+ // Output: <description><![CDATA[This content has <special> characters & more]]></description>
337
+ ```
338
+
339
+ ### Raw XML Content
340
+
341
+ Use `DangerouslyInsertRawXml` for pre-rendered HTML/XML content:
342
+
343
+ ```tsx
344
+ import { DangerouslyInsertRawXml } from '@navios/adapter-xml'
345
+
346
+ const contentEncoded = defineTag('content:encoded')
347
+
348
+ const htmlContent = '<p>Hello <strong>World</strong></p>'
349
+
350
+ <contentEncoded>
351
+ <DangerouslyInsertRawXml>{htmlContent}</DangerouslyInsertRawXml>
352
+ </contentEncoded>
353
+ // Output: <content:encoded><p>Hello <strong>World</strong></p></content:encoded>
354
+ ```
355
+
356
+ **Warning:** `DangerouslyInsertRawXml` bypasses all XML escaping. Only use with trusted content.
357
+
358
+ ## API Reference
359
+
360
+ ### `defineTag(name, propsSchema?)`
361
+
362
+ Creates a type-safe XML tag component.
363
+
364
+ - `name` - Tag name (supports namespaces like `atom:link`)
365
+ - `propsSchema` - Optional Zod schema for props validation
366
+
367
+ ### `declareXmlStream(config)`
368
+
369
+ Declares an XML stream endpoint.
370
+
371
+ ```typescript
372
+ interface BaseXmlStreamConfig {
373
+ method: HttpMethod
374
+ url: string
375
+ querySchema?: ZodType
376
+ requestSchema?: ZodType
377
+ contentType?:
378
+ | 'application/xml'
379
+ | 'text/xml'
380
+ | 'application/rss+xml'
381
+ | 'application/atom+xml'
382
+ xmlDeclaration?: boolean // Include <?xml?> declaration (default: true)
383
+ encoding?: string // XML encoding (default: 'UTF-8')
384
+ }
385
+ ```
386
+
387
+ ### `@XmlStream(endpoint)`
388
+
389
+ Decorator for controller methods that return XML.
390
+
391
+ ### `defineXmlEnvironment()`
392
+
393
+ Returns environment configuration to merge with your base adapter.
394
+
395
+ ### `@Component(options?)`
396
+
397
+ Decorator for class-based XML components with dependency injection support.
398
+
399
+ ```typescript
400
+ // Without props
401
+ @Component()
402
+ class MyComponent implements XmlComponent { ... }
403
+
404
+ // With props schema
405
+ @Component({ schema: MyPropsSchema })
406
+ class MyComponent implements XmlComponent { ... }
407
+
408
+ // With custom registry
409
+ @Component({ registry: customRegistry })
410
+ class MyComponent implements XmlComponent { ... }
411
+ ```
412
+
413
+ ### `XmlComponent`
414
+
415
+ Interface for class components. Must implement a `render()` method.
416
+
417
+ ```typescript
418
+ interface XmlComponent {
419
+ render(): AnyXmlNode | Promise<AnyXmlNode>
420
+ }
421
+ ```
422
+
423
+ ### `CData`
424
+
425
+ Component for CDATA sections. Automatically handles content containing `]]>`.
426
+
427
+ ### `DangerouslyInsertRawXml`
428
+
429
+ Component for inserting raw XML/HTML without escaping.
430
+
431
+ ### `renderToXml(node, options?)`
432
+
433
+ Low-level function to render JSX to XML string.
434
+
435
+ ```typescript
436
+ interface RenderOptions {
437
+ declaration?: boolean // Include XML declaration
438
+ encoding?: string // XML encoding
439
+ pretty?: boolean // Pretty print output
440
+ container?: Container // DI container for class components
441
+ }
442
+ ```
443
+
444
+ ## Content Types
445
+
446
+ The adapter supports these content types:
447
+
448
+ - `application/xml` (default)
449
+ - `text/xml`
450
+ - `application/rss+xml`
451
+ - `application/atom+xml`
452
+
453
+ ## Examples
454
+
455
+ ### Sitemap
456
+
457
+ ```tsx
458
+ const urlset = defineTag('urlset', z.object({
459
+ xmlns: z.string(),
460
+ }))
461
+ const url = defineTag('url')
462
+ const loc = defineTag('loc')
463
+ const lastmod = defineTag('lastmod')
464
+ const changefreq = defineTag('changefreq')
465
+ const priority = defineTag('priority')
466
+
467
+ @XmlStream(getSitemapDefinition)
468
+ async getSitemap() {
469
+ const pages = await this.getPages()
470
+
471
+ return (
472
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
473
+ {pages.map(page => (
474
+ <url>
475
+ <loc>{page.url}</loc>
476
+ <lastmod>{page.updatedAt.toISOString()}</lastmod>
477
+ <changefreq>weekly</changefreq>
478
+ <priority>{page.priority}</priority>
479
+ </url>
480
+ ))}
481
+ </urlset>
482
+ )
483
+ }
484
+ ```
485
+
486
+ ### Atom Feed
487
+
488
+ ```tsx
489
+ const feed = defineTag('feed', z.object({
490
+ xmlns: z.string(),
491
+ }))
492
+ const entry = defineTag('entry')
493
+ const id = defineTag('id')
494
+ const updated = defineTag('updated')
495
+ const author = defineTag('author')
496
+ const name = defineTag('name')
497
+ const content = defineTag('content', z.object({
498
+ type: z.string().optional(),
499
+ }))
500
+
501
+ @XmlStream(getAtomFeedDefinition)
502
+ async getAtomFeed() {
503
+ const posts = await this.getPosts()
504
+
505
+ return (
506
+ <feed xmlns="http://www.w3.org/2005/Atom">
507
+ <title>My Blog</title>
508
+ <link href="https://example.com" rel="alternate" />
509
+ <id>urn:uuid:blog-feed-id</id>
510
+ <updated>{new Date().toISOString()}</updated>
511
+ {posts.map(post => (
512
+ <entry>
513
+ <title>{post.title}</title>
514
+ <link href={post.url} rel="alternate" />
515
+ <id>{post.id}</id>
516
+ <updated>{post.updatedAt.toISOString()}</updated>
517
+ <author>
518
+ <name>{post.author}</name>
519
+ </author>
520
+ <content type="html">
521
+ <CData>{post.content}</CData>
522
+ </content>
523
+ </entry>
524
+ ))}
525
+ </feed>
526
+ )
527
+ }
528
+ ```
529
+
530
+ ## License
531
+
532
+ MIT
package/bun-plugin.mts ADDED
@@ -0,0 +1,129 @@
1
+ import fs from 'fs'
2
+
3
+ import * as babel from '@babel/core'
4
+ import { plugin } from 'bun'
5
+ import chalk from 'chalk'
6
+
7
+ // 0. minor setup
8
+ let totalTimeSpentTranspiling = 0
9
+ const RELOAD_HACK_FILENAME = 'bunPlugin.reload.ts'
10
+
11
+ const startSetup = Date.now()
12
+ const log = (str: string): void => console.log(chalk.dim(`[bun] ${str}`))
13
+ log(`evaluating plugin module`)
14
+
15
+ plugin({
16
+ name: 'typescript-with-native-decorators-and-jsx',
17
+ setup(build): void {
18
+ // 1. watch files manually since --watch is broken when using bun plugin
19
+ const folderToWatch = import.meta.dir + '/src/'
20
+ log(`manually watching ${folderToWatch}`)
21
+ const watchFileSet = new Set<string | null>()
22
+ fs.watch(folderToWatch, { recursive: true }, (event, filename) => {
23
+ const needestart = watchFileSet.has(filename)
24
+ log(`file ${filename} changed (in set: ${watchFileSet.has(filename)})`)
25
+ if (needestart)
26
+ void Bun.file(`./${RELOAD_HACK_FILENAME}`).write(
27
+ `// '${Math.random()})\n`,
28
+ )
29
+ })
30
+
31
+ // 2. change how .ts and .tsx files are loaded
32
+ log(`setup took ${Date.now() - startSetup}ms`)
33
+ const filter = /.*\.m?(ts|tsx)$/
34
+ build.onLoad(
35
+ { filter },
36
+ async (args): Promise<{ loader: 'js'; contents: string }> => {
37
+ try {
38
+ watchFileSet.add(args.path.replace(folderToWatch, ''))
39
+ const codeTs = await Bun.file(args.path).text()
40
+ const startTranspiling = performance.now()
41
+ let codeJS = await transpileFile(args.path, codeTs)
42
+
43
+ totalTimeSpentTranspiling += performance.now() - startTranspiling
44
+ addToCache(codeTs, codeJS)
45
+ return { loader: 'js', contents: codeJS }
46
+ } catch (e) {
47
+ console.log(`[🔴] `, e)
48
+ return { contents: '', loader: 'js' }
49
+ }
50
+ },
51
+ )
52
+ },
53
+ })
54
+
55
+ // 4. cache system
56
+ const oldCache: Map<string, string> = (await Bun.file(
57
+ './bunPlugin.cache',
58
+ ).exists())
59
+ ? new Map(JSON.parse((await Bun.file('./bunPlugin.cache').text()) || '[]'))
60
+ : new Map()
61
+
62
+ const cache = new Map<string, string>()
63
+ log(`restoring module with ${oldCache.size} entries`)
64
+
65
+ export const getFromCache = (content: string): string | undefined => {
66
+ if (cache.has(content)) return cache.get(content)
67
+ if (oldCache.has(content)) {
68
+ const value = oldCache.get(content)
69
+ if (value) cache.set(content, value)
70
+ return value
71
+ }
72
+ return
73
+ }
74
+ export function debounce<F extends (...args: any[]) => void>(
75
+ func: F,
76
+ waitFor: number,
77
+ ) {
78
+ let timeout: ReturnType<typeof setTimeout> | null = null
79
+
80
+ const debounced = (...args: Parameters<F>) => {
81
+ if (timeout) {
82
+ clearTimeout(timeout)
83
+ }
84
+ timeout = setTimeout(() => func(...args), waitFor)
85
+ }
86
+
87
+ return debounced as (...args: Parameters<F>) => void
88
+ }
89
+
90
+ const saveCacheImpl = debounce(() => {
91
+ log(`saving cache with ${cache.size} entries (spent ${totalTimeSpentTranspiling.toFixed(2)}ms transpiling)`)
92
+ void Bun.file('./bunPlugin.cache').write(JSON.stringify([...cache.entries()]))
93
+ }, 50)
94
+
95
+ export const addToCache = (codeTS: string, codeJS: string): void => {
96
+ cache.set(codeTS, codeJS)
97
+ saveCacheImpl()
98
+ }
99
+
100
+ async function transpileFile(name: string, codeTs: string): Promise<string> {
101
+ // 1. try to return the cached version
102
+ const cache = getFromCache(codeTs)
103
+ if (cache) return cache
104
+
105
+ // or transpile a new
106
+ const codeJs = await transpileFileForReal(name, codeTs)
107
+ addToCache(codeTs, codeJs)
108
+ return codeJs
109
+ }
110
+
111
+ async function transpileFileForReal(
112
+ name: string,
113
+ codeTS: string,
114
+ ): Promise<string> {
115
+ // Use esbuild with JSX support
116
+ const esbuild = (await import('esbuild')).default
117
+
118
+ // Determine loader based on file extension and content
119
+ const hasJsx = codeTS.includes('<') && (codeTS.includes('/>') || codeTS.includes('</'))
120
+ const loader = hasJsx ? 'tsx' : 'ts'
121
+
122
+ const result = await esbuild.transform(codeTS, {
123
+ loader,
124
+ target: 'chrome110',
125
+ jsx: 'automatic',
126
+ jsxImportSource: '@navios/adapter-xml',
127
+ })
128
+ return result.code
129
+ }