diff --git a/packages/core/package.json b/packages/core/package.json
index d9d4abfa3baa..329e8b6bee29 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -94,7 +94,6 @@
"@smithy/util-body-length-browser": "^4.1.0",
"@smithy/util-middleware": "^4.1.1",
"@smithy/util-utf8": "^4.1.0",
- "fast-xml-parser": "5.2.5",
"tslib": "^2.6.2"
},
"devDependencies": {
diff --git a/packages/core/src/submodules/protocols/xml/XmlShapeDeserializer.ts b/packages/core/src/submodules/protocols/xml/XmlShapeDeserializer.ts
index 9aea2a91ee69..212ab2da183b 100644
--- a/packages/core/src/submodules/protocols/xml/XmlShapeDeserializer.ts
+++ b/packages/core/src/submodules/protocols/xml/XmlShapeDeserializer.ts
@@ -1,9 +1,9 @@
+import { parseXML } from "@aws-sdk/xml-builder";
import { FromStringShapeDeserializer } from "@smithy/core/protocols";
import { NormalizedSchema } from "@smithy/core/schema";
import { getValueFromTextNode } from "@smithy/smithy-client";
import type { Schema, SerdeFunctions, ShapeDeserializer } from "@smithy/types";
import { toUtf8 } from "@smithy/util-utf8";
-import { XMLParser } from "fast-xml-parser";
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
import type { XmlSettings } from "./XmlCodec";
@@ -146,21 +146,9 @@ export class XmlShapeDeserializer extends SerdeContextConfig implements ShapeDes
protected parseXml(xml: string): any {
if (xml.length) {
- const parser = new XMLParser({
- attributeNamePrefix: "",
- htmlEntities: true,
- ignoreAttributes: false,
- ignoreDeclaration: true,
- parseTagValue: false,
- trimValues: false,
- tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
- });
- parser.addEntity("#xD", "\r");
- parser.addEntity("#10", "\n");
-
let parsedObj;
try {
- parsedObj = parser.parse(xml, true);
+ parsedObj = parseXML(xml);
} catch (e: any) {
if (e && typeof e === "object") {
Object.defineProperty(e, "$responseBodyText", {
diff --git a/packages/core/src/submodules/protocols/xml/parseXmlBody.ts b/packages/core/src/submodules/protocols/xml/parseXmlBody.ts
index 2d5bac5999a0..85ef329c0cd7 100644
--- a/packages/core/src/submodules/protocols/xml/parseXmlBody.ts
+++ b/packages/core/src/submodules/protocols/xml/parseXmlBody.ts
@@ -1,6 +1,6 @@
+import { parseXML } from "@aws-sdk/xml-builder";
import { getValueFromTextNode } from "@smithy/smithy-client";
import type { HttpResponse, SerdeContext } from "@smithy/types";
-import { XMLParser } from "fast-xml-parser";
import { collectBodyString } from "../common";
@@ -10,21 +10,9 @@ import { collectBodyString } from "../common";
export const parseXmlBody = (streamBody: any, context: SerdeContext): any =>
collectBodyString(streamBody, context).then((encoded) => {
if (encoded.length) {
- const parser = new XMLParser({
- attributeNamePrefix: "",
- htmlEntities: true,
- ignoreAttributes: false,
- ignoreDeclaration: true,
- parseTagValue: false,
- trimValues: false,
- tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
- });
- parser.addEntity("#xD", "\r");
- parser.addEntity("#10", "\n");
-
let parsedObj;
try {
- parsedObj = parser.parse(encoded, true);
+ parsedObj = parseXML(encoded);
} catch (e: any) {
if (e && typeof e === "object") {
Object.defineProperty(e, "$responseBodyText", {
diff --git a/packages/xml-builder/package.json b/packages/xml-builder/package.json
index b7f4e13a1954..f5cccfd083d5 100644
--- a/packages/xml-builder/package.json
+++ b/packages/xml-builder/package.json
@@ -4,6 +4,7 @@
"description": "XML builder for the AWS SDK",
"dependencies": {
"@smithy/types": "^4.5.0",
+ "fast-xml-parser": "5.2.5",
"tslib": "^2.6.2"
},
"scripts": {
@@ -38,6 +39,13 @@
"files": [
"dist-*/**"
],
+ "browser": {
+ "./dist-es/xml-parser": "./dist-es/xml-parser.browser"
+ },
+ "react-native": {
+ "./dist-es/xml-parser": "./dist-es/xml-parser",
+ "./dist-cjs/xml-parser": "./dist-cjs/xml-parser"
+ },
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/xml-builder",
"repository": {
"type": "git",
diff --git a/packages/xml-builder/src/index.ts b/packages/xml-builder/src/index.ts
index ed9933828403..dc7fd5f908f4 100644
--- a/packages/xml-builder/src/index.ts
+++ b/packages/xml-builder/src/index.ts
@@ -6,3 +6,8 @@ export * from "./XmlNode";
* @internal
*/
export * from "./XmlText";
+
+/**
+ * @internal
+ */
+export { parseXML } from "./xml-parser";
diff --git a/packages/xml-builder/src/xml-parser.browser.ts b/packages/xml-builder/src/xml-parser.browser.ts
new file mode 100644
index 000000000000..7104cb33ecb3
--- /dev/null
+++ b/packages/xml-builder/src/xml-parser.browser.ts
@@ -0,0 +1,56 @@
+const parser = new DOMParser();
+
+export function parseXML(xmlString: string): any {
+ const xmlDocument = parser.parseFromString(xmlString, "application/xml");
+
+ // Recursive function to convert XML nodes to JS object
+ const xmlToObj = (node: Node): any => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ if (node.textContent?.trim()) {
+ return node.textContent;
+ }
+ }
+
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element;
+ if (element.attributes.length === 0 && element.childNodes.length === 0) {
+ return "";
+ }
+
+ const obj: any = {};
+
+ for (const attr of Array.from(element.attributes)) {
+ obj[`${attr.name}`] = attr.value;
+ }
+
+ for (const child of Array.from(element.childNodes)) {
+ const childResult = xmlToObj(child);
+
+ if (childResult != null) {
+ const childName = child.nodeName;
+ if (childName === "#text") {
+ return childResult;
+ }
+
+ if (obj[childName]) {
+ if (Array.isArray(obj[childName])) {
+ obj[childName].push(childResult);
+ } else {
+ obj[childName] = [obj[childName], childResult];
+ }
+ } else {
+ obj[childName] = childResult;
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ return null;
+ };
+
+ return {
+ [xmlDocument.documentElement.nodeName]: xmlToObj(xmlDocument.documentElement),
+ };
+}
diff --git a/packages/xml-builder/src/xml-parser.spec.ts b/packages/xml-builder/src/xml-parser.spec.ts
new file mode 100644
index 000000000000..b5b95fcc4b31
--- /dev/null
+++ b/packages/xml-builder/src/xml-parser.spec.ts
@@ -0,0 +1,120 @@
+import { describe, expect, test as it } from "vitest";
+
+import { parseXML } from "./xml-parser";
+import { parseXML as parseXMLBrowser } from "./xml-parser.browser";
+
+describe("xml parsing", () => {
+ for (const { name, parse } of [
+ { name: "fast-xml-parser", parse: parseXML },
+ { name: "DOMParser", parse: parseXMLBrowser },
+ ]) {
+ describe(name, () => {
+ it("should parse a valid xml string without xml header", () => {
+ const xml = `
+
+
+ STS_AR_ACCESS_KEY_ID
+ STS_AR_SECRET_ACCESS_KEY
+ STS_AR_SESSION_TOKEN_us-west-2
+ 3000-01-01T00:00:00.000Z
+
+
+
+ 01234567-89ab-cdef-0123-456789abcdef
+
+`;
+ const object = parse(xml);
+ expect(object).toEqual({
+ AssumeRoleResponse: {
+ AssumeRoleResult: {
+ Credentials: {
+ AccessKeyId: "STS_AR_ACCESS_KEY_ID",
+ Expiration: "3000-01-01T00:00:00.000Z",
+ SecretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
+ SessionToken: "STS_AR_SESSION_TOKEN_us-west-2",
+ },
+ },
+ ResponseMetadata: {
+ RequestId: "01234567-89ab-cdef-0123-456789abcdef",
+ },
+ xmlns: "https://sts.amazonaws.com/doc/2011-06-15/",
+ },
+ });
+ });
+
+ it("should parse ListBuckets response XML with xml header", () => {
+ const xml = `
+
+
+
+ string
+ string
+ timestamp
+ string
+
+
+
+ string
+ string
+
+ string
+ string
+`;
+ const object = parse(xml);
+ expect(object).toEqual({
+ ListAllMyBucketsResult: {
+ Buckets: {
+ Bucket: {
+ BucketArn: "string",
+ BucketRegion: "string",
+ CreationDate: "timestamp",
+ Name: "string",
+ },
+ },
+ ContinuationToken: "string",
+ Owner: {
+ DisplayName: "string",
+ ID: "string",
+ },
+ Prefix: "string",
+ },
+ });
+ });
+
+ it("should parse xml (custom)", () => {
+ const xml = `
+
+
+ abcdefg
+ dup1
+ dup2
+ dup3
+ s p a c e d
+
+
+ abcdefg
+ dup1
+ dup2
+ dup3
+ s p a c e d
+
+`;
+ const object = parse(xml);
+ expect(object).toEqual({
+ struct: {
+ empty: "",
+ text: "abcdefg",
+ duplicate: ["dup1", "dup2", "dup3"],
+ spaced: " s p a c e d ",
+ nested: {
+ empty: "",
+ text: "abcdefg",
+ duplicate: ["dup1", "dup2", "dup3"],
+ spaced: " s p a c e d ",
+ },
+ },
+ });
+ });
+ });
+ }
+});
diff --git a/packages/xml-builder/src/xml-parser.ts b/packages/xml-builder/src/xml-parser.ts
new file mode 100644
index 000000000000..5e2aba24a2af
--- /dev/null
+++ b/packages/xml-builder/src/xml-parser.ts
@@ -0,0 +1,17 @@
+import { XMLParser } from "fast-xml-parser";
+
+const parser = new XMLParser({
+ attributeNamePrefix: "",
+ htmlEntities: true,
+ ignoreAttributes: false,
+ ignoreDeclaration: true,
+ parseTagValue: false,
+ trimValues: false,
+ tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
+});
+parser.addEntity("#xD", "\r");
+parser.addEntity("#10", "\n");
+
+export function parseXML(xmlString: string): any {
+ return parser.parse(xmlString, true);
+}
diff --git a/packages/xml-builder/vitest.config.mts b/packages/xml-builder/vitest.config.mts
index 4e46707824a5..73fcc11c3178 100644
--- a/packages/xml-builder/vitest.config.mts
+++ b/packages/xml-builder/vitest.config.mts
@@ -4,6 +4,6 @@ export default defineConfig({
test: {
exclude: ["**/*.{integ,e2e,browser}.spec.ts"],
include: ["**/*.spec.ts"],
- environment: "node",
+ environment: "happy-dom",
},
});
diff --git a/yarn.lock b/yarn.lock
index 26bf2867a4eb..44aa15b27508 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23534,7 +23534,6 @@ __metadata:
"@tsconfig/recommended": "npm:1.0.1"
concurrently: "npm:7.0.0"
downlevel-dts: "npm:0.10.1"
- fast-xml-parser: "npm:5.2.5"
rimraf: "npm:3.0.2"
tslib: "npm:^2.6.2"
typescript: "npm:~5.8.3"
@@ -25005,6 +25004,7 @@ __metadata:
"@tsconfig/recommended": "npm:1.0.1"
concurrently: "npm:7.0.0"
downlevel-dts: "npm:0.10.1"
+ fast-xml-parser: "npm:5.2.5"
rimraf: "npm:3.0.2"
tslib: "npm:^2.6.2"
typescript: "npm:~5.8.3"