Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@

*.lockb binary diff=lockb

# XML loader fixtures in specific byte encodings (BOMs, UTF-16, ISO-8859-1);
# any newline or encoding normalization would change what they test.
test/js/bun/resolve/xml/xml-utf16le-bom.xml -text
test/js/bun/resolve/xml/xml-utf16be-bom.xml -text
test/js/bun/resolve/xml/xml-utf8-bom.xml -text
test/js/bun/resolve/xml/xml-latin1.xml -text

.vscode/launch.json linguist-generated
src/api/schema.d.ts linguist-generated
fixture.*.c linguist-generated
Expand Down
36 changes: 36 additions & 0 deletions bench/xml/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions bench/xml/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "xml-benchmark",
"version": "1.0.0",
"dependencies": {
"fast-xml-parser": "^5.2.5",
"xml2js": "^0.6.2"
}
}
100 changes: 100 additions & 0 deletions bench/xml/xml.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { XMLBuilder, XMLParser } from "fast-xml-parser";
import xml2js from "xml2js";
import { bench, group, run } from "../runner.mjs";

const isBun = typeof Bun !== "undefined" && Bun.XML;

function sizeLabel(n) {
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`;
if (n >= 1024) return `${(n / 1024).toFixed(0)}KB`;
return `${n}B`;
}

// -- parse inputs --

const small = `<?xml version="1.0" encoding="UTF-8"?>
<user id="42" active="true">
<name>John Doe</name>
<email>john@example.com</email>
<roles><role>admin</role><role>editor</role></roles>
</user>`;

// An S3 ListObjectsV2-style response.
function listing(count) {
const parts = [
`<?xml version="1.0" encoding="UTF-8"?>\n<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">\n <Name>bucket</Name>\n <Prefix/>\n <KeyCount>${count}</KeyCount>\n <MaxKeys>1000</MaxKeys>\n <IsTruncated>false</IsTruncated>\n`,
];
for (let i = 0; i < count; i++) {
parts.push(` <Contents>
<Key>photos/2024/${i.toString(16)}/image_${i}.jpg</Key>
<LastModified>2024-01-${String((i % 28) + 1).padStart(2, "0")}T12:00:00.000Z</LastModified>
<ETag>&quot;${(i * 2654435761).toString(16)}&quot;</ETag>
<Size>${(i * 7919) % 1000000}</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>\n`);
}
parts.push(`</ListBucketResult>\n`);
return parts.join("");
}

// An Atom-feed-style document with mixed content and CDATA.
function feed(count) {
const parts = [
`<?xml version="1.0" encoding="utf-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <title>Example Feed</title>\n <updated>2024-01-13T18:30:02Z</updated>\n`,
];
for (let i = 0; i < count; i++) {
parts.push(` <entry>
<title type="html">Post number ${i} &amp; other &lt;things&gt;</title>
<link href="http://example.org/2024/01/${i}" rel="alternate"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-${String(i).padStart(12, "0")}</id>
<updated>2024-01-13T18:30:02Z</updated>
<summary><![CDATA[<p>Some <b>HTML</b> in entry ${i}, kept verbatim.</p>]]></summary>
<author><name>Author ${i % 7}</name></author>
</entry>\n`);
}
parts.push(`</feed>\n`);
return parts.join("");
}

const large = listing(1000);
const mixed = feed(500);

const fxp = new XMLParser({ ignoreAttributes: false, parseTagValue: false });
const parseXml2js = doc => {
let result;
xml2js.parseString(doc, (err, r) => {
if (err) throw err;
result = r;
});
return result;
};

for (const [label, doc] of [
["small", small],
["S3 listing", large],
["Atom feed", mixed],
]) {
group(`parse ${label} (${sizeLabel(doc.length)})`, () => {
if (isBun) {
bench("Bun.XML.parse", () => Bun.XML.parse(doc));
bench("Bun.XML.parse { compact: false }", () => Bun.XML.parse(doc, { compact: false }));
}
bench("fast-xml-parser", () => fxp.parse(doc));
bench("xml2js", () => parseXml2js(doc));
});
}

// -- stringify --

// Each serializer gets the object its own parser produced ("@" vs "@_"
// attribute keys), so both do the same work.
const bunObject = isBun ? Bun.XML.parse(large) : undefined;
const fxpObject = fxp.parse(large);
const builder = new XMLBuilder({ ignoreAttributes: false });

group(`stringify S3 listing`, () => {
if (isBun) bench("Bun.XML.stringify", () => Bun.XML.stringify(bunObject));
bench("fast-xml-parser XMLBuilder", () => builder.build(fxpObject));
});

await run();
53 changes: 53 additions & 0 deletions docs/bundler/loaders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,59 @@ export default {

---

### `xml`

**XML loader.** Default for `.xml`.

XML files can be directly imported. Bun parses them with its native XML 1.0 parser into the compact object shape of [`Bun.XML.parse`](/runtime/xml): one key for the root element, `"@name"` keys for attributes, arrays for repeated child elements, `"#text"` for text next to attributes or children, and every value a string.

```ts
import doc from "./config.xml";
console.log(doc.config["@version"]);

// via import attribute:
import feed from "./export.rss" with { type: "xml" };
```

During bundling, the parsed XML is inlined into the bundle as a JavaScript object.

```ts
var doc = {
config: {
"@version": "2",
// ...other fields
},
};
```

If a `.xml` file is passed as an entrypoint, it is converted to a `.js` module that `export default`s the parsed object.

<CodeGroup>

```xml Input
<user id="1">
<name>John Doe</name>
<email>johndoe@example.com</email>
<role>admin</role>
<role>editor</role>
</user>
```

```ts Output
export default {
user: {
"@id": "1",
name: "John Doe",
email: "johndoe@example.com",
role: ["admin", "editor"],
},
};
```

</CodeGroup>

---

### `text`

**Text loader.** Default for `.txt`.
Expand Down
2 changes: 2 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"/runtime/yaml",
"/runtime/markdown",
"/runtime/json5",
"/runtime/xml",
"/runtime/jsonl",
"/runtime/html-rewriter",
"/runtime/image",
Expand Down Expand Up @@ -513,6 +514,7 @@
"/guides/runtime/import-toml",
"/guides/runtime/import-yaml",
"/guides/runtime/import-json5",
"/guides/runtime/import-xml",
"/guides/runtime/import-html",
"/guides/util/import-meta-dir",
"/guides/util/import-meta-file",
Expand Down
63 changes: 63 additions & 0 deletions docs/guides/runtime/import-xml.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Import an XML file
sidebarTitle: Import XML
mode: center
---

Bun natively supports `.xml` imports.

```xml config.xml icon="file-code"
<?xml version="1.0" encoding="UTF-8"?>
<config env="production">
<database host="localhost" port="5432" name="myapp"/>
<server port="3000" timeout="30"/>
<feature name="auth"/>
<feature name="rateLimit"/>
</config>
```

---

Import the file like any other source file. The module is the parsed document: one key for the root element, `"@name"` keys for attributes, arrays for repeated elements, and every value a string.

```ts config.ts icon="/icons/typescript.svg"
import doc from "./config.xml";

doc.config["@env"]; // => "production"
doc.config.database["@host"]; // => "localhost"
doc.config.server["@port"]; // => "3000"
doc.config.feature.map(f => f["@name"]); // => ["auth", "rateLimit"]
```

---

The root element is also available as a named import:

```ts config.ts icon="/icons/typescript.svg"
import { config } from "./config.xml";

console.log(config.database["@name"]); // => "myapp"
console.log(Number(config.server["@timeout"])); // => 30
```

---

For parsing XML strings at runtime, use `Bun.XML.parse()`:

```ts config.ts icon="/icons/typescript.svg"
const data = Bun.XML.parse(`
<user id="7">
<name>John Doe</name>
<hobby>reading</hobby>
<hobby>coding</hobby>
</user>
`);

console.log(data.user.name); // => "John Doe"
console.log(data.user.hobby); // => ["reading", "coding"]
console.log(data.user["@id"]); // => "7"
```

---

See [XML](/runtime/xml) for the rest of Bun's XML support, including the ordered `{ compact: false }` node tree and `Bun.XML.stringify()`.
2 changes: 1 addition & 1 deletion docs/runtime/bun-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ Use the links in the table to jump to the associated documentation.
| Stream Processing | [`Bun.readableStreamTo*()`](/runtime/utils#bun-readablestreamto), `Bun.readableStreamToBytes()`, `Bun.readableStreamToBlob()`, `Bun.readableStreamToFormData()`, `Bun.readableStreamToJSON()`, `Bun.readableStreamToArray()` |
| Memory & Buffer Management | `Bun.ArrayBufferSink`, `Bun.allocUnsafe`, `Bun.concatArrayBuffers` |
| Module Resolution | [`Bun.resolveSync()`](/runtime/utils#bun-resolvesync) |
| Parsing & Formatting | [`Bun.semver`](/runtime/semver), [`Bun.TOML.parse`](/runtime/toml), [`Bun.markdown`](/runtime/markdown), [`Bun.color`](/runtime/color), [`Bun.Image`](/runtime/image) |
| Parsing & Formatting | [`Bun.semver`](/runtime/semver), [`Bun.TOML.parse`](/runtime/toml), [`Bun.XML`](/runtime/xml), [`Bun.markdown`](/runtime/markdown), [`Bun.color`](/runtime/color), [`Bun.Image`](/runtime/image) |
| Low-level / Internals | `Bun.mmap`, `Bun.gc`, `Bun.generateHeapSnapshot`, [`bun:jsc`](https://bun.com/reference/bun/jsc) |
53 changes: 52 additions & 1 deletion docs/runtime/file-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "File types and loaders supported by Bun's bundler and runtime"

The Bun bundler implements a set of default loaders. As a rule of thumb, the bundler and the runtime support the same set of file types.

`.js` `.cjs` `.mjs` `.mts` `.cts` `.ts` `.tsx` `.jsx` `.css` `.json` `.jsonc` `.json5` `.toml` `.yaml` `.yml` `.txt` `.wasm` `.node` `.html` `.sh`
`.js` `.cjs` `.mjs` `.mts` `.cts` `.ts` `.tsx` `.jsx` `.css` `.json` `.jsonc` `.json5` `.toml` `.yaml` `.yml` `.xml` `.txt` `.wasm` `.node` `.html` `.sh`

Bun uses the file extension to pick the built-in _loader_ that parses the file. Every loader has a name, such as `js`, `tsx`, or `json`. These names are used when building [plugins](/bundler/plugins) that extend Bun with custom loaders.

Expand Down Expand Up @@ -244,6 +244,57 @@ export default {

</CodeGroup>

### `xml`

**XML loader**. Default for `.xml`.

XML files can be directly imported. Bun parses them with its native XML 1.0 parser into the compact object shape of [`Bun.XML.parse`](/runtime/xml): one key for the root element, `"@name"` keys for attributes, arrays for repeated child elements, `"#text"` for text next to attributes or children, and every value a string.

```ts
import doc from "./config.xml";
console.log(doc.config["@version"]);

// via import attribute:
import feed from "./export.rss" with { type: "xml" };
```

During bundling, the parsed XML is inlined into the bundle as a JavaScript object.

```ts
var doc = {
config: {
"@version": "2",
// ...other fields
},
};
```

If a `.xml` file is passed as an entrypoint, it is converted to a `.js` module that `export default`s the parsed object.

<CodeGroup>

```xml Input
<user id="1">
<name>John Doe</name>
<email>johndoe@example.com</email>
<role>admin</role>
<role>editor</role>
</user>
```

```ts Output
export default {
user: {
"@id": "1",
name: "John Doe",
email: "johndoe@example.com",
role: ["admin", "editor"],
},
};
```

</CodeGroup>

### `text`

**Text loader**. Default for `.txt`.
Expand Down
Loading