npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hqtsm/plist

v2.0.1

Published

Property list file encoding and decoding

Readme

HQTSM: Plist

Property list file encoding and decoding

plist encode decode openstep xml binary

JSR npm CI

Features

  • Pure TypeScript, run anywhere
  • Zero dependencies
  • Tree shaking friendly design
  • Support for both encoding and decoding multiple formats
    • OpenStep / Strings
    • XML
    • Binary
  • Optional support for non-UTF XML encodings (BYO decoder)
  • Fast custom parsers that correctly handle the edge cases of offical parsers
  • Well tested against hundreds of samples generated from official libraries
  • Additionally tested against samples that offical libraries do not produce
  • Support for obscure data types supported by official libraries
  • Supports encoding output identical to official libraries
  • Encoders and decoders preserve the data structure as closely as possible
  • Includes a type-safe walk function to walk a property list

Usage

Encode Binary

import {
	encode,
	encodeBinary,
	FORMAT_BINARY_V1_0,
	PLDictionary,
	PLInteger,
	PLString,
} from '@hqtsm/plist';

const plist = new PLDictionary();
plist.set(new PLString('Name'), new PLString('John Smith'));
plist.set(new PLString('Age'), new PLInteger(42n));

const expected = `
	62 70 6C 69 73 74 30 30 D2 01 02 03 04 54 4E 61
	6D 65 53 41 67 65 5A 4A 6F 68 6E 20 53 6D 69 74
	68 10 2A 08 0D 12 16 21 00 00 00 00 00 00 01 01
	00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00
	00 00 00 00 00 00 00 23
	`.trim().split(/\s+/).map((s) => parseInt(s, 16));

// OR: encodeBinary(plist);
const enc = encode(plist, { format: FORMAT_BINARY_V1_0 });
console.assert(String.fromCharCode(...enc.slice(0, 8)) === 'bplist00');
console.assert(JSON.stringify([...enc]) === JSON.stringify(expected));

NOTE: It is possible to encode types that official libraries to not support encoding and/or decoding (null type, set type, non-string keys, non-primitive keys).

Encode Binary Options

Option: format ('BINARY-V1.0')

Only FORMAT_BINARY_V1_0 is valid for binary.

Option: duplicates (Iterable<PLTypeName | PLType>)

A list of types or values to be duplicated in the offset table. Only useful to create a 1:1 identical encode as an official encoder.

Encode XML

import {
	encode,
	FORMAT_XML_V1_0,
	PLDictionary,
	PLInteger,
	PLString,
	PLUID,
} from '@hqtsm/plist';

const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));
plist.set(new PLString('Age'), new PLInteger(42n));
plist.set(new PLString('ID'), new PLUID(1234n));

const expected = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>First Name</key>
	<string>John</string>
	<key>Last Name</key>
	<string>Smith</string>
	<key>Age</key>
	<integer>42</integer>
	<key>ID</key>
	<dict>
		<key>CF$UID</key>
		<integer>1234</integer>
	</dict>
</dict>
</plist>
`;

// OR: encodeXml(plist);
const enc = encode(plist, { format: FORMAT_XML_V1_0 });
console.assert(new TextDecoder().decode(enc) === expected);

NOTE: Not every type is supported in the XML format. An exception will throw for null type, set type, and non-string keys.

Encode XML Options

Option: format ('XML-V1.0' | 'XML-V0.9')

Either FORMAT_XML_V1_0 or FORMAT_XML_V0_9 (different headers).

Option: indent (string)

A custom indent strings of tabs or spaces.

Option: unsignZero (boolean)

Encode -0.0 as just 0.0 as official encoders do.

Option: min128Zero (boolean)

Encode smallest 128-bit integer as -0 as official encoders do. 128-bit integers are a private API in official encoders, limited compatibility.

Encode OpenStep / Strings

import { encode, FORMAT_OPENSTEP, PLDictionary, PLString } from '@hqtsm/plist';

const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));

const expected = `{
	"First Name" = John;
	"Last Name" = Smith;
}
`;

// OR: encodeOpenStep(plist);
const enc = encode(plist, { format: FORMAT_OPENSTEP });
console.assert(new TextDecoder().decode(enc) === expected);
import { encode, FORMAT_STRINGS, PLDictionary, PLString } from '@hqtsm/plist';

const plist = new PLDictionary();
plist.set(new PLString('First Name'), new PLString('John'));
plist.set(new PLString('Last Name'), new PLString('Smith'));

const expected = `"First Name" = John;
"Last Name" = Smith;
`;
// OR: encodeOpenStep(plist, { format: FORMAT_STRINGS });
const enc = encode(plist, { format: FORMAT_STRINGS });
console.assert(new TextDecoder().decode(enc) === expected);

NOTE: Not every type is supported in the OpenStep formats. An exception will throw for any values not a dictionary, array, string, or data type, and for keys that are not string type. With the strings format only the dictionary type can be encoded.

Encode OpenStep / Strings Options

Option: format ('OPENSTEP' | 'STRINGS')

Either FORMAT_OPENSTEP or FORMAT_STRINGS.

Option: indent (string)

A custom indent strings of tabs or spaces.

Option: quote ('"' | "'")

Set the string quote character.

Option: quoted (boolean)

Option to always make strings quoted even when it is optional.

Option: shortcut (boolean)

Use the "shortcut" style for keys and values that are reference equal.

{
	"shortcut";
}

VS

{
	"shortcut" = "shortcut";
}

Decode Binary

import { decode, FORMAT_BINARY_V1_0, PLDictionary, walk } from '@hqtsm/plist';

const encoded = new Uint8Array(
	`
	62 70 6C 69 73 74 30 30 D2 01 02 03 04 54 4E 61
	6D 65 53 41 67 65 5A 4A 6F 68 6E 20 53 6D 69 74
	68 10 2A 08 0D 12 16 21 00 00 00 00 00 00 01 01
	00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 00
	00 00 00 00 00 00 00 23
	`.trim().split(/\s+/).map((s) => parseInt(s, 16)),
);

// OR: decodeBinary(encoded):
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_BINARY_V1_0);
console.assert(PLDictionary.is(plist));
walk(plist, {
	PLDictionary(dict) {
		console.assert(dict.size === 2);
	},
	PLString(str, _, key) {
		if (key) {
			console.assert(str.value === 'John Smith');
		} else {
			const keys = ['Name', 'Age'];
			console.assert(keys.includes(str.value));
		}
	},
	PLInteger(int) {
		console.assert(int.value === 42n);
	},
});

Decode Binary Options

Option: int64 (boolean)

Optionally limit integers to the range of 64-bit signed or unsigned values. 128-bit integers in official decoders is limited to unsigned 64-bit values.

Option: primitiveKeys (boolean)

Optionally limit key types to primitive types. The open source CF encoder does this.

Option: stringKeys (boolean)

Optionally limit key types to string type. The closed source CF encoder does this.

Decode XML

import { decode, FORMAT_XML_V1_0, PLDictionary, walk } from '@hqtsm/plist';

const encoded = new TextEncoder().encode(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>First Name</key>
	<string>John</string>
	<key>Last Name</key>
	<string>Smith</string>
	<key>Age</key>
	<integer>42</integer>
	<key>ID</key>
	<dict>
		<key>CF$UID</key>
		<integer>1234</integer>
	</dict>
</dict>
</plist>
`);

// OR: decodeXml(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_XML_V1_0);
console.assert(PLDictionary.is(plist));
walk(plist, {
	PLDictionary(dict) {
		console.assert(dict.size === 4);
	},
	PLString(str, _, key) {
		if (key) {
			const values = ['John', 'Smith'];
			console.assert(values.includes(str.value));
		} else {
			const keys = ['First Name', 'Last Name', 'Age', 'ID'];
			console.assert(keys.includes(str.value));
		}
	},
	PLInteger(int) {
		console.assert(int.value === 42n);
	},
	PLUID(int) {
		console.assert(int.value === 1234n);
	},
});

Decode XML Options

Option: decoded (boolean)

Flag to skip decoding and assumed UTF-8 without BOM.

Option: decoder (DecodeXmlDecoder)

Optonal decoder function for converting XML to UTF-8 based on the header. Useful to decode an XML document with a non-UTF encoding. Official encoders always use UTF-8 but many encodings can be decoded depending on what the host platform has available. Using TextDecoder can be helpful, but it may be different in some edge cases.

Option: int64 (boolean)

Optionally limit integers to the range of 64-bit signed or unsigned values. 128-bit integers in official decoders is limited to unsigned 64-bit values.

Option: utf16le (boolean)

Optional UTF-16 endian flag when no BOM available. Defaults to auto detect based on which character is null. Official decoders assume it will match host endian.

Decode OpenStep / Strings

import { decode, FORMAT_OPENSTEP, PLDictionary, walk } from '@hqtsm/plist';

const encoded = new TextEncoder().encode(`{
	"First Name" = John;
	"Last Name" = Smith;
}
`);

// OR: decodeOpenStep(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_OPENSTEP);
console.assert(PLDictionary.is(plist));
walk(plist, {
	PLDictionary(dict) {
		console.assert(dict.size === 2);
	},
	PLString(str, _, key) {
		if (key) {
			const values = ['John', 'Smith'];
			console.assert(values.includes(str.value));
		} else {
			const keys = ['First Name', 'Last Name'];
			console.assert(keys.includes(str.value));
		}
	},
});
import { decode, FORMAT_STRINGS, PLDictionary, walk } from '@hqtsm/plist';

const encoded = new TextEncoder().encode(`"First Name" = John;
"Last Name" = Smith;
`);

// OR: decodeOpenStep(encoded);
const { format, plist } = decode(encoded);
console.assert(format === FORMAT_STRINGS);
console.assert(PLDictionary.is(plist));
walk(plist, {
	PLDictionary(dict) {
		console.assert(dict.size === 2);
	},
	PLString(str, _, key) {
		if (key) {
			const values = ['John', 'Smith'];
			console.assert(values.includes(str.value));
		} else {
			const keys = ['First Name', 'Last Name'];
			console.assert(keys.includes(str.value));
		}
	},
});

Decode OpenStep / Strings Options

Option: allowMissingSemi (boolean)

Allow missing semicolon on the last dictionary item. Official decoders once allowed this but deprecated and removed it.

Option: decoded (boolean)

Flag to skip decoding and assumed UTF-8 without BOM. OpenStep does not store encoding information so UTF is assumed by decoders. If another encoding is used it must first be converted to UTF before decoding.

Option: utf16le (boolean)

Optional UTF-16 endian flag when no BOM available. Defaults to auto detect based on which character is null. Official decoders assume it will match host endian.