@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/project.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@navios/adapter-xml",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/adapter-xml/src",
5
+ "prefix": "adapter-xml",
6
+ "tags": [],
7
+ "projectType": "library",
8
+ "targets": {
9
+ "check": {
10
+ "executor": "nx:run-commands",
11
+ "outputs": ["{projectRoot}/dist"],
12
+ "inputs": [
13
+ "^projectSources",
14
+ "projectSources",
15
+ "{projectRoot}/tsconfig.json",
16
+ "{projectRoot}/tsconfig.lib.json"
17
+ ],
18
+ "options": {
19
+ "command": ["tsc -b"],
20
+ "cwd": "packages/adapter-xml"
21
+ }
22
+ },
23
+ "lint": {
24
+ "executor": "nx:run-commands",
25
+ "inputs": ["^projectSources", "project"],
26
+ "options": {
27
+ "command": "oxlint --fix",
28
+ "cwd": "packages/adapter-xml"
29
+ }
30
+ },
31
+ "test:ci": {
32
+ "executor": "nx:run-commands",
33
+ "inputs": ["^projectSources", "project"],
34
+ "dependsOn": ["test:ci:unit", "test:ci:fastify", "test:ci:bun"],
35
+ "options": {
36
+ "command": "echo 'All tests passed'",
37
+ "cwd": "packages/adapter-xml"
38
+ }
39
+ },
40
+ "test:ci:unit": {
41
+ "executor": "nx:run-commands",
42
+ "inputs": ["^projectSources", "project"],
43
+ "options": {
44
+ "command": "vitest run --config vitest.config.mts src/",
45
+ "cwd": "packages/adapter-xml"
46
+ }
47
+ },
48
+ "test:ci:fastify": {
49
+ "executor": "nx:run-commands",
50
+ "inputs": ["^projectSources", "project"],
51
+ "options": {
52
+ "command": "vitest run --config vitest.e2e.fastify.config.mts",
53
+ "cwd": "packages/adapter-xml"
54
+ }
55
+ },
56
+ "test:ci:bun": {
57
+ "executor": "nx:run-commands",
58
+ "inputs": ["^projectSources", "project"],
59
+ "options": {
60
+ "command": "bun test e2e/bun/",
61
+ "cwd": "packages/adapter-xml"
62
+ }
63
+ },
64
+ "build": {
65
+ "executor": "nx:run-commands",
66
+ "inputs": ["projectSources", "{projectRoot}/tsup.config.mts"],
67
+ "outputs": ["{projectRoot}/lib"],
68
+ "dependsOn": ["check", "test:ci", "lint"],
69
+ "options": {
70
+ "command": "tsup",
71
+ "cwd": "packages/adapter-xml"
72
+ }
73
+ },
74
+ "publish": {
75
+ "executor": "nx:run-commands",
76
+ "dependsOn": ["build"],
77
+ "options": {
78
+ "command": "yarn npm publish --access public",
79
+ "cwd": "packages/adapter-xml"
80
+ }
81
+ },
82
+ "publish:next": {
83
+ "executor": "nx:run-commands",
84
+ "dependsOn": ["build"],
85
+ "options": {
86
+ "command": "yarn npm publish --access public --tag next",
87
+ "cwd": "packages/adapter-xml"
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1 @@
1
+ export { XmlStreamAdapterService } from './xml-stream-adapter.service.mjs'
@@ -0,0 +1,121 @@
1
+ import type {
2
+ AbstractHttpHandlerAdapterInterface,
3
+ HandlerMetadata,
4
+ } from '@navios/core'
5
+ import type { ClassType, RequestContextHolder } from '@navios/di'
6
+
7
+ import { StreamAdapterToken, XmlStreamAdapterToken } from '@navios/core'
8
+ import { Container, inject, Injectable } from '@navios/di'
9
+
10
+ import type { BaseXmlStreamConfig } from '../types/config.mjs'
11
+ import type { AnyXmlNode } from '../types/xml-node.mjs'
12
+
13
+ import { renderToXml } from '../runtime/render-to-xml.mjs'
14
+
15
+ @Injectable({
16
+ token: XmlStreamAdapterToken,
17
+ })
18
+ export class XmlStreamAdapterService implements AbstractHttpHandlerAdapterInterface {
19
+ protected container = inject(Container)
20
+ /** Base stream adapter - we proxy hasSchema, prepareArguments, provideSchema to it */
21
+ protected streamAdapter = inject(StreamAdapterToken)
22
+
23
+ /**
24
+ * Proxy to base StreamAdapter - reuses existing argument preparation logic
25
+ * (handles querySchema, requestSchema, URL params for both Fastify and Bun)
26
+ */
27
+ prepareArguments(handlerMetadata: HandlerMetadata<BaseXmlStreamConfig>) {
28
+ return this.streamAdapter.prepareArguments?.(handlerMetadata) ?? []
29
+ }
30
+
31
+ provideSchema(
32
+ handlerMetadata: HandlerMetadata<BaseXmlStreamConfig>,
33
+ ): Record<string, any> {
34
+ if (
35
+ 'provideSchema' in this.streamAdapter &&
36
+ typeof this.streamAdapter.provideSchema === 'function'
37
+ ) {
38
+ return this.streamAdapter.provideSchema(handlerMetadata)
39
+ }
40
+ return {}
41
+ }
42
+
43
+ hasSchema(handlerMetadata: HandlerMetadata<any>): boolean {
44
+ if (
45
+ 'hasSchema' in this.streamAdapter &&
46
+ typeof this.streamAdapter.hasSchema === 'function'
47
+ ) {
48
+ return this.streamAdapter.hasSchema(handlerMetadata)
49
+ }
50
+ return false
51
+ }
52
+
53
+ /**
54
+ * Custom handler - renders JSX to XML and handles response for both Fastify and Bun
55
+ */
56
+ provideHandler(
57
+ controller: ClassType,
58
+ handlerMetadata: HandlerMetadata<BaseXmlStreamConfig>,
59
+ ): (context: RequestContextHolder, request: any, reply: any) => Promise<any> {
60
+ const getters = this.prepareArguments(handlerMetadata)
61
+ const config = handlerMetadata.config
62
+
63
+ const formatArguments = async (request: any) => {
64
+ const argument: Record<string, any> = {}
65
+ const promises: Promise<void>[] = []
66
+ for (const getter of getters) {
67
+ const res = getter(argument, request)
68
+ if (res instanceof Promise) {
69
+ promises.push(res)
70
+ }
71
+ }
72
+ await Promise.all(promises)
73
+ return argument
74
+ }
75
+
76
+ const contentType = config.contentType ?? 'application/xml'
77
+ const renderOptions = {
78
+ declaration: config.xmlDeclaration ?? true,
79
+ encoding: config.encoding ?? 'UTF-8',
80
+ container: this.container,
81
+ }
82
+
83
+ return async (context: RequestContextHolder, request: any, reply: any) => {
84
+ const controllerInstance = await this.container.get(controller)
85
+ const argument = await formatArguments(request)
86
+
87
+ // Call controller method - returns XmlNode (JSX), may contain async/class components
88
+ const xmlNode: AnyXmlNode =
89
+ await controllerInstance[handlerMetadata.classMethod](argument)
90
+
91
+ // Render JSX to XML string (async - resolves all async and class components)
92
+ const xml = await renderToXml(xmlNode, renderOptions)
93
+
94
+ // Environment detection: Bun doesn't have reply
95
+ const isHttpStandardEnvironment = reply === undefined
96
+
97
+ if (isHttpStandardEnvironment) {
98
+ // Bun: return Response object
99
+ const headers: Record<string, string> = {
100
+ 'Content-Type': contentType,
101
+ }
102
+ for (const [key, value] of Object.entries(handlerMetadata.headers)) {
103
+ if (value != null) {
104
+ headers[key] = String(value)
105
+ }
106
+ }
107
+ return new Response(xml, {
108
+ status: handlerMetadata.successStatusCode,
109
+ headers,
110
+ })
111
+ } else {
112
+ // Fastify: use reply object
113
+ reply
114
+ .status(handlerMetadata.successStatusCode)
115
+ .header('Content-Type', contentType)
116
+ .headers(handlerMetadata.headers)
117
+ .send(xml)
118
+ }
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,102 @@
1
+ import type { z, ZodObject, ZodRawShape } from 'zod/v4'
2
+
3
+ import type { Registry } from '@navios/di'
4
+
5
+ import {
6
+ InjectableScope,
7
+ InjectableType,
8
+ InjectableTokenMeta,
9
+ InjectionToken,
10
+ globalRegistry,
11
+ } from '@navios/di'
12
+
13
+ import type { ComponentClass, XmlComponent } from '../types/component.mjs'
14
+
15
+ export const ComponentMeta = Symbol.for('xml.component.meta')
16
+
17
+ // #1 Component without props (no schema)
18
+ export function Component(): <T extends ComponentClass>(
19
+ target: T,
20
+ context?: ClassDecoratorContext,
21
+ ) => T
22
+
23
+ // #2 Component with props schema
24
+ export function Component<Schema extends ZodObject<ZodRawShape>>(options: {
25
+ schema: Schema
26
+ registry?: Registry
27
+ }): <T extends new (props: z.output<Schema>, ...args: any[]) => XmlComponent>(
28
+ target: T,
29
+ context?: ClassDecoratorContext,
30
+ ) => T
31
+
32
+ // #3 Component with custom registry only
33
+ export function Component(options: {
34
+ registry: Registry
35
+ }): <T extends ComponentClass>(
36
+ target: T,
37
+ context?: ClassDecoratorContext,
38
+ ) => T
39
+
40
+ export function Component(
41
+ options: {
42
+ schema?: ZodObject<ZodRawShape>
43
+ registry?: Registry
44
+ } = {},
45
+ ) {
46
+ const { schema, registry = globalRegistry } = options
47
+
48
+ return <T extends ComponentClass>(
49
+ target: T,
50
+ context?: ClassDecoratorContext,
51
+ ): T => {
52
+ if (
53
+ (context && context.kind !== 'class') ||
54
+ (target instanceof Function && !context)
55
+ ) {
56
+ throw new Error(
57
+ '[@navios/adapter-xml] @Component decorator can only be used on classes.',
58
+ )
59
+ }
60
+
61
+ // Verify the class has a render method
62
+ if (typeof target.prototype.render !== 'function') {
63
+ throw new Error(
64
+ `[@navios/adapter-xml] @Component class "${target.name}" must implement render() method.`,
65
+ )
66
+ }
67
+
68
+ // Create token with schema if provided
69
+ const injectableToken = schema
70
+ ? InjectionToken.create(target, schema)
71
+ : InjectionToken.create(target)
72
+
73
+ // Register with Request scope - each render gets fresh instances
74
+ registry.set(
75
+ injectableToken,
76
+ InjectableScope.Request,
77
+ target,
78
+ InjectableType.Class,
79
+ )
80
+
81
+ // Store token metadata on the class (same pattern as @Injectable)
82
+ // @ts-expect-error - Adding metadata to class
83
+ target[InjectableTokenMeta] = injectableToken
84
+
85
+ // Mark as component for JSX runtime detection
86
+ // @ts-expect-error - Adding metadata to class
87
+ target[ComponentMeta] = true
88
+
89
+ return target
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Type guard to check if a class is a component
95
+ */
96
+ export function isComponentClass(value: unknown): value is ComponentClass {
97
+ return (
98
+ typeof value === 'function' &&
99
+ // @ts-expect-error - Checking metadata
100
+ value[ComponentMeta] === true
101
+ )
102
+ }
@@ -0,0 +1,345 @@
1
+ import { Container, Registry } from '@navios/di'
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+ import { z } from 'zod/v4'
5
+
6
+ import type { XmlComponent } from '../types/component.mjs'
7
+
8
+ import { createElement } from '../runtime/create-element.mjs'
9
+ import { renderToXml } from '../runtime/render-to-xml.mjs'
10
+ import { ClassComponent } from '../types/xml-node.mjs'
11
+ import {
12
+ Component,
13
+ ComponentMeta,
14
+ isComponentClass,
15
+ } from './component.decorator.mjs'
16
+
17
+ describe('@Component decorator', () => {
18
+ it('should mark class as a component', () => {
19
+ @Component()
20
+ class TestComponent implements XmlComponent {
21
+ render() {
22
+ return createElement('test', null)
23
+ }
24
+ }
25
+
26
+ expect(isComponentClass(TestComponent)).toBe(true)
27
+ // @ts-expect-error - Checking metadata
28
+ expect(TestComponent[ComponentMeta]).toBe(true)
29
+ })
30
+
31
+ it('should throw if class does not have render method', () => {
32
+ expect(() => {
33
+ // @ts-expect-error - Testing invalid class
34
+ @Component()
35
+ class InvalidComponent {
36
+ notRender() {
37
+ return null
38
+ }
39
+ }
40
+ // Force evaluation
41
+ return InvalidComponent
42
+ }).toThrow('must implement render() method')
43
+ })
44
+
45
+ it('should create ClassComponentNode when used in createElement', () => {
46
+ @Component()
47
+ class MyComponent implements XmlComponent {
48
+ render() {
49
+ return createElement('content', null, 'Hello')
50
+ }
51
+ }
52
+
53
+ const node = createElement(MyComponent, null)
54
+
55
+ expect(node).toEqual({
56
+ type: ClassComponent,
57
+ componentClass: MyComponent,
58
+ props: { children: [] },
59
+ })
60
+ })
61
+
62
+ it('should pass props to ClassComponentNode', () => {
63
+ const PropsSchema = z.object({
64
+ name: z.string(),
65
+ age: z.number().optional(),
66
+ })
67
+
68
+ @Component({ schema: PropsSchema })
69
+ class PropsComponent implements XmlComponent {
70
+ constructor(private props: z.output<typeof PropsSchema>) {}
71
+
72
+ render() {
73
+ return createElement('user', null, this.props.name)
74
+ }
75
+ }
76
+
77
+ const node = createElement(PropsComponent, { name: 'John', age: 30 })
78
+
79
+ expect(node).toEqual({
80
+ type: ClassComponent,
81
+ componentClass: PropsComponent,
82
+ props: { name: 'John', age: 30, children: [] },
83
+ })
84
+ })
85
+
86
+ it('should allow custom registry', () => {
87
+ const customRegistry = new Registry()
88
+
89
+ @Component({ registry: customRegistry })
90
+ class RegistryComponent implements XmlComponent {
91
+ render() {
92
+ return createElement('custom', null)
93
+ }
94
+ }
95
+
96
+ expect(isComponentClass(RegistryComponent)).toBe(true)
97
+ // The component should have a token stored
98
+ // @ts-expect-error - Checking InjectableTokenMeta
99
+ const token = RegistryComponent[Symbol.for('InjectableTokenMeta')]
100
+ expect(token).toBeDefined()
101
+ // And that token should be registered in the custom registry
102
+ expect(customRegistry.has(token)).toBe(true)
103
+ })
104
+ })
105
+
106
+ describe('isComponentClass', () => {
107
+ it('should return true for decorated classes', () => {
108
+ @Component()
109
+ class DecoratedComponent implements XmlComponent {
110
+ render() {
111
+ return createElement('test', null)
112
+ }
113
+ }
114
+
115
+ expect(isComponentClass(DecoratedComponent)).toBe(true)
116
+ })
117
+
118
+ it('should return false for regular functions', () => {
119
+ function regularFunction() {
120
+ return createElement('test', null)
121
+ }
122
+
123
+ expect(isComponentClass(regularFunction)).toBe(false)
124
+ })
125
+
126
+ it('should return false for regular classes', () => {
127
+ class RegularClass {
128
+ render() {
129
+ return createElement('test', null)
130
+ }
131
+ }
132
+
133
+ expect(isComponentClass(RegularClass)).toBe(false)
134
+ })
135
+
136
+ it('should return false for non-functions', () => {
137
+ expect(isComponentClass('string')).toBe(false)
138
+ expect(isComponentClass(123)).toBe(false)
139
+ expect(isComponentClass(null)).toBe(false)
140
+ expect(isComponentClass(undefined)).toBe(false)
141
+ expect(isComponentClass({})).toBe(false)
142
+ })
143
+ })
144
+
145
+ describe('class component rendering', () => {
146
+ it('should render basic class component', async () => {
147
+ @Component()
148
+ class SimpleComponent implements XmlComponent {
149
+ render() {
150
+ return createElement('simple', null, 'content')
151
+ }
152
+ }
153
+
154
+ const container = new Container()
155
+ container.beginRequest('test-request')
156
+ try {
157
+ const node = createElement(SimpleComponent, null)
158
+ const xml = await renderToXml(node, { declaration: false, container })
159
+ expect(xml).toBe('<simple>content</simple>')
160
+ } finally {
161
+ await container.endRequest('test-request')
162
+ }
163
+ })
164
+
165
+ it('should render class component with props', async () => {
166
+ const GreetingSchema = z.object({
167
+ name: z.string(),
168
+ })
169
+
170
+ @Component({ schema: GreetingSchema })
171
+ class Greeting implements XmlComponent {
172
+ constructor(private props: z.output<typeof GreetingSchema>) {}
173
+
174
+ render() {
175
+ return createElement('greeting', null, `Hello, ${this.props.name}!`)
176
+ }
177
+ }
178
+
179
+ const container = new Container()
180
+ container.beginRequest('test-request')
181
+ try {
182
+ const node = createElement(Greeting, { name: 'World' })
183
+ const xml = await renderToXml(node, { declaration: false, container })
184
+ expect(xml).toBe('<greeting>Hello, World!</greeting>')
185
+ } finally {
186
+ await container.endRequest('test-request')
187
+ }
188
+ })
189
+
190
+ it('should render nested class components', async () => {
191
+ @Component()
192
+ class Inner implements XmlComponent {
193
+ render() {
194
+ return createElement('inner', null, 'nested')
195
+ }
196
+ }
197
+
198
+ @Component()
199
+ class Outer implements XmlComponent {
200
+ render() {
201
+ return createElement('outer', null, createElement(Inner, null))
202
+ }
203
+ }
204
+
205
+ const container = new Container()
206
+ container.beginRequest('test-request')
207
+ try {
208
+ const node = createElement(Outer, null)
209
+ const xml = await renderToXml(node, { declaration: false, container })
210
+ expect(xml).toBe('<outer><inner>nested</inner></outer>')
211
+ } finally {
212
+ await container.endRequest('test-request')
213
+ }
214
+ })
215
+
216
+ it('should render async class component', async () => {
217
+ @Component()
218
+ class AsyncComponent implements XmlComponent {
219
+ async render() {
220
+ await new Promise((resolve) => setTimeout(resolve, 10))
221
+ return createElement('async', null, 'loaded')
222
+ }
223
+ }
224
+
225
+ const container = new Container()
226
+ container.beginRequest('test-request')
227
+ try {
228
+ const node = createElement(AsyncComponent, null)
229
+ const xml = await renderToXml(node, { declaration: false, container })
230
+ expect(xml).toBe('<async>loaded</async>')
231
+ } finally {
232
+ await container.endRequest('test-request')
233
+ }
234
+ })
235
+
236
+ it('should throw MissingContainerError when container not provided', async () => {
237
+ @Component()
238
+ class RequiresContainer implements XmlComponent {
239
+ render() {
240
+ return createElement('test', null)
241
+ }
242
+ }
243
+
244
+ const node = createElement(RequiresContainer, null)
245
+
246
+ await expect(renderToXml(node, { declaration: false })).rejects.toThrow(
247
+ 'Cannot render class component "RequiresContainer" without a container',
248
+ )
249
+ })
250
+
251
+ it('should validate props with Zod schema', async () => {
252
+ const StrictSchema = z.object({
253
+ count: z.number().min(0),
254
+ })
255
+
256
+ @Component({ schema: StrictSchema })
257
+ class StrictComponent implements XmlComponent {
258
+ constructor(private props: z.output<typeof StrictSchema>) {}
259
+
260
+ render() {
261
+ return createElement('count', null, String(this.props.count))
262
+ }
263
+ }
264
+
265
+ const container = new Container()
266
+ container.beginRequest('test-request')
267
+ try {
268
+ // Valid props
269
+ const validNode = createElement(StrictComponent, { count: 5 })
270
+ const xml = await renderToXml(validNode, {
271
+ declaration: false,
272
+ container,
273
+ })
274
+ expect(xml).toBe('<count>5</count>')
275
+
276
+ // Invalid props - negative number
277
+ const invalidNode = createElement(StrictComponent, { count: -1 })
278
+ await expect(
279
+ renderToXml(invalidNode, { declaration: false, container }),
280
+ ).rejects.toThrow()
281
+ } finally {
282
+ await container.endRequest('test-request')
283
+ }
284
+ })
285
+
286
+ it('should mix functional and class components', async () => {
287
+ function Header({ title }: { title: string }) {
288
+ return createElement('header', null, title)
289
+ }
290
+
291
+ @Component()
292
+ class Footer implements XmlComponent {
293
+ render() {
294
+ return createElement('footer', null, 'Copyright 2025')
295
+ }
296
+ }
297
+
298
+ const container = new Container()
299
+ container.beginRequest('test-request')
300
+ try {
301
+ const node = createElement(
302
+ 'page',
303
+ null,
304
+ createElement(Header, { title: 'Welcome' }),
305
+ createElement(Footer, null),
306
+ )
307
+ const xml = await renderToXml(node, { declaration: false, container })
308
+ expect(xml).toBe(
309
+ '<page><header>Welcome</header><footer>Copyright 2025</footer></page>',
310
+ )
311
+ } finally {
312
+ await container.endRequest('test-request')
313
+ }
314
+ })
315
+
316
+ it('should handle children in class components', async () => {
317
+ const WrapperSchema = z.object({
318
+ children: z.array(z.any()).optional(),
319
+ })
320
+
321
+ @Component({ schema: WrapperSchema })
322
+ class Wrapper implements XmlComponent {
323
+ constructor(private props: z.output<typeof WrapperSchema>) {}
324
+
325
+ render() {
326
+ return createElement('wrapper', null, ...(this.props.children ?? []))
327
+ }
328
+ }
329
+
330
+ const container = new Container()
331
+ container.beginRequest('test-request')
332
+ try {
333
+ const node = createElement(
334
+ Wrapper,
335
+ null,
336
+ createElement('child1', null),
337
+ createElement('child2', null),
338
+ )
339
+ const xml = await renderToXml(node, { declaration: false, container })
340
+ expect(xml).toBe('<wrapper><child1/><child2/></wrapper>')
341
+ } finally {
342
+ await container.endRequest('test-request')
343
+ }
344
+ })
345
+ })
@@ -0,0 +1,4 @@
1
+ export { XmlStream } from './xml-stream.decorator.mjs'
2
+ export type { XmlStreamParams } from './xml-stream.decorator.mjs'
3
+
4
+ export { Component, ComponentMeta, isComponentClass } from './component.decorator.mjs'