@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/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
|
+
})
|