@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.
- package/LICENSE +7 -0
- package/README.md +532 -0
- package/bun-plugin.mts +129 -0
- package/bunPlugin.cache +1 -0
- package/bunfig.toml +3 -0
- package/dist/bun-plugin.d.mts +4 -0
- package/dist/bun-plugin.d.mts.map +1 -0
- package/dist/e2e/bun/xml-stream.spec.d.ts +2 -0
- package/dist/e2e/bun/xml-stream.spec.d.ts.map +1 -0
- package/dist/e2e/fastify/xml-stream.spec.d.ts +2 -0
- package/dist/e2e/fastify/xml-stream.spec.d.ts.map +1 -0
- package/dist/src/adapters/index.d.mts +2 -0
- package/dist/src/adapters/index.d.mts.map +1 -0
- package/dist/src/adapters/xml-stream-adapter.service.d.mts +21 -0
- package/dist/src/adapters/xml-stream-adapter.service.d.mts.map +1 -0
- package/dist/src/decorators/component.decorator.d.mts +17 -0
- package/dist/src/decorators/component.decorator.d.mts.map +1 -0
- package/dist/src/decorators/component.decorator.spec.d.mts +2 -0
- package/dist/src/decorators/component.decorator.spec.d.mts.map +1 -0
- package/dist/src/decorators/index.d.mts +4 -0
- package/dist/src/decorators/index.d.mts.map +1 -0
- package/dist/src/decorators/xml-stream.decorator.d.mts +42 -0
- package/dist/src/decorators/xml-stream.decorator.d.mts.map +1 -0
- package/dist/src/define-environment.d.mts +31 -0
- package/dist/src/define-environment.d.mts.map +1 -0
- package/dist/src/handlers/index.d.mts +2 -0
- package/dist/src/handlers/index.d.mts.map +1 -0
- package/dist/src/handlers/xml-stream.d.mts +23 -0
- package/dist/src/handlers/xml-stream.d.mts.map +1 -0
- package/dist/src/index.d.mts +12 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/jsx-dev-runtime.d.mts +5 -0
- package/dist/src/jsx-dev-runtime.d.mts.map +1 -0
- package/dist/src/jsx-runtime.d.mts +3 -0
- package/dist/src/jsx-runtime.d.mts.map +1 -0
- package/dist/src/jsx.d.mts +18 -0
- package/dist/src/jsx.d.mts.map +1 -0
- package/dist/src/runtime/create-element.d.mts +25 -0
- package/dist/src/runtime/create-element.d.mts.map +1 -0
- package/dist/src/runtime/fragment.d.mts +2 -0
- package/dist/src/runtime/fragment.d.mts.map +1 -0
- package/dist/src/runtime/index.d.mts +5 -0
- package/dist/src/runtime/index.d.mts.map +1 -0
- package/dist/src/runtime/render-to-xml.d.mts +20 -0
- package/dist/src/runtime/render-to-xml.d.mts.map +1 -0
- package/dist/src/runtime/render-to-xml.spec.d.mts +2 -0
- package/dist/src/runtime/render-to-xml.spec.d.mts.map +1 -0
- package/dist/src/runtime/special-nodes.d.mts +24 -0
- package/dist/src/runtime/special-nodes.d.mts.map +1 -0
- package/dist/src/tags/define-tag.d.mts +33 -0
- package/dist/src/tags/define-tag.d.mts.map +1 -0
- package/dist/src/tags/define-tag.spec.d.mts +2 -0
- package/dist/src/tags/define-tag.spec.d.mts.map +1 -0
- package/dist/src/tags/index.d.mts +3 -0
- package/dist/src/tags/index.d.mts.map +1 -0
- package/dist/src/types/component.d.mts +15 -0
- package/dist/src/types/component.d.mts.map +1 -0
- package/dist/src/types/config.d.mts +10 -0
- package/dist/src/types/config.d.mts.map +1 -0
- package/dist/src/types/index.d.mts +5 -0
- package/dist/src/types/index.d.mts.map +1 -0
- package/dist/src/types/xml-node.d.mts +35 -0
- package/dist/src/types/xml-node.d.mts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/tsup.config.d.mts +3 -0
- package/dist/tsup.config.d.mts.map +1 -0
- package/dist/vitest.config.d.mts +3 -0
- package/dist/vitest.config.d.mts.map +1 -0
- package/dist/vitest.e2e.fastify.config.d.mts +3 -0
- package/dist/vitest.e2e.fastify.config.d.mts.map +1 -0
- package/e2e/bun/xml-stream.spec.tsx +553 -0
- package/e2e/fastify/xml-stream.spec.tsx +569 -0
- package/jsx.d.ts +42 -0
- package/lib/_tsup-dts-rollup.d.mts +414 -0
- package/lib/_tsup-dts-rollup.d.ts +414 -0
- package/lib/chunk-6OR6LGJA.mjs +153 -0
- package/lib/chunk-6OR6LGJA.mjs.map +1 -0
- package/lib/index.d.mts +29 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +376 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +256 -0
- package/lib/index.mjs.map +1 -0
- package/lib/jsx-dev-runtime.d.mts +4 -0
- package/lib/jsx-dev-runtime.d.ts +4 -0
- package/lib/jsx-dev-runtime.js +61 -0
- package/lib/jsx-dev-runtime.js.map +1 -0
- package/lib/jsx-dev-runtime.mjs +9 -0
- package/lib/jsx-dev-runtime.mjs.map +1 -0
- package/lib/jsx-runtime.d.mts +3 -0
- package/lib/jsx-runtime.d.ts +3 -0
- package/lib/jsx-runtime.js +57 -0
- package/lib/jsx-runtime.js.map +1 -0
- package/lib/jsx-runtime.mjs +3 -0
- package/lib/jsx-runtime.mjs.map +1 -0
- package/lib/jsx.d.mts +1 -0
- package/lib/jsx.d.ts +1 -0
- package/lib/jsx.js +4 -0
- package/lib/jsx.js.map +1 -0
- package/lib/jsx.mjs +3 -0
- package/lib/jsx.mjs.map +1 -0
- package/package.json +80 -0
- package/project.json +91 -0
- package/src/adapters/index.mts +1 -0
- package/src/adapters/xml-stream-adapter.service.mts +121 -0
- package/src/decorators/component.decorator.mts +102 -0
- package/src/decorators/component.decorator.spec.mts +345 -0
- package/src/decorators/index.mts +4 -0
- package/src/decorators/xml-stream.decorator.mts +93 -0
- package/src/define-environment.mts +40 -0
- package/src/handlers/index.mts +1 -0
- package/src/handlers/xml-stream.mts +31 -0
- package/src/index.mts +41 -0
- package/src/jsx-dev-runtime.mts +8 -0
- package/src/jsx-runtime.mts +2 -0
- package/src/jsx.mts +25 -0
- package/src/runtime/create-element.mts +113 -0
- package/src/runtime/fragment.mts +1 -0
- package/src/runtime/index.mts +4 -0
- package/src/runtime/render-to-xml.mts +214 -0
- package/src/runtime/render-to-xml.spec.mts +360 -0
- package/src/runtime/special-nodes.mts +32 -0
- package/src/tags/define-tag.mts +54 -0
- package/src/tags/define-tag.spec.mts +250 -0
- package/src/tags/index.mts +2 -0
- package/src/types/component.mts +16 -0
- package/src/types/config.mts +15 -0
- package/src/types/index.mts +23 -0
- package/src/types/jsx.d.ts +21 -0
- package/src/types/xml-node.mts +50 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +25 -0
- package/tsup.config.mts +18 -0
- package/vitest.config.mts +9 -0
- 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
|
+
}
|