elide:yaml parses, streams and writes YAML 1.2. Parsed documents are plain data: mappings are objects,
sequences are arrays and scalars are primitives. There is nothing to install.
import { parse, stringify } from "elide:yaml";
const config = parse(`
port: 8080
hosts: [a, b]
tls:
cert: ./cert.pem
`);
config.port; // 8080
config.hosts; // ["a", "b"]
stringify(config);
// "port: 8080\nhosts:\n - a\n - b\ntls:\n cert: ./cert.pem\n"
Import it as elide:yaml. A bare "yaml" specifier resolves to the npm package of that name.
Read a file
# team.yaml
team: platform
users:
- id: 1
name: ada
- id: 2
name: lin
// team.mjs
import { parseFile } from "elide:yaml";
const { team, users } = parseFile("team.yaml");
for (const user of users) console.log(team, user.id, user.name);
elide run team.mjs prints:
platform 1 ada
platform 2 lin
Parsing
parse(input, options?)reads a string,Uint8Array,ArrayBufferorDataView(UTF-8, or a BOM-marked encoding) holding one document and returns its value. Empty input isnull; input holding several documents throws.parseAll(input, options?)returns every document of a stream as an array.parseDocument(input, options?)andparseAllDocuments(input, options?)return documents, which carry their directives.parseFile(path, options?)reads one document from a file path orfile:URL.stream(source, options?)reads documents one at a time from a path,file:URL, bytes or aReadableStreamof bytes. See Streaming.
Values follow the YAML 1.2 core schema: true/false are booleans, null/~/empty are null,
decimal, 0x and 0o integers and floats (including .inf and .nan) are numbers, and everything
else is a string. Integers outside ±(253−1) are bigints. Keys are always strings, so
1: one gives { "1": "one" }.
parse("a: 1\nb: 1.5\nc: yes\nd: 2024-01-01\n");
// { a: 1, b: 1.5, c: "yes", d: "2024-01-01" }
parse("big: 18446744073709551616"); // { big: 18446744073709551616n }
Anchors, aliases and merge keys
Aliases refer to the value built for their anchor, so *ref is the same object as &ref. An alias
to a node that contains it is rejected with ERR_YAML_SYNTAX. Merge keys (<<) are folded into their
mapping with merge: true, or by default when reading YAML 1.1; otherwise << is an ordinary key.
const v = parse("base: &b\n x: 1\nref: *b\n");
v.ref === v.base; // true
parse("base: &b\n a: 1\n b: 2\nchild:\n <<: *b\n b: 3\n", { merge: true }).child;
// { a: 1, b: 3 }
Tags
!!binary reads as a Uint8Array and !!timestamp as a Date. Other tags leave the value as the
schema parses it; with unknownTags: "error" they throw instead. customTags maps a tag to a
function that receives the parsed value and the tag and returns what to use:
parse("db: !secret DATABASE_URL", {
customTags: { "!secret": (name) => process.env[name] },
});
Tag URIs are given in full for the standard tags ("tag:yaml.org,2002:timestamp") and as written
for local tags ("!secret").
Options
| Option | Default | Meaning |
|---|---|---|
version | "1.2" | "1.1" selects the YAML 1.1 schema and merge keys |
schema | "core" | "json" (JSON forms only), "failsafe" (strings only) or "yaml-1.1" (yes/no, 010, 1_000) |
merge | false | Fold << merge keys |
uniqueKeys | true | Reject a mapping that repeats a key; false keeps the last value |
aliases | true | Allow anchors and aliases |
unknownTags | "keep" | "error" rejects tags the schema does not know |
customTags | none | Tag handlers, by tag |
limits | see below | Resource bounds |
limits bounds maxBytes (input size, 64 MiB), maxNodes (one million), maxDepth (128),
maxAliases (10 000) and maxAliasExpansion (nodes aliases may expand to, one million).
Documents
parseDocument() returns the document’s contents, the %YAML directive as version (or null)
and %TAG prefixes by handle as tags, with toJS() and toString(options?):
import { parseDocument } from "elide:yaml";
const doc = parseDocument("%YAML 1.2\n---\nname: elide\n");
doc.contents; // { name: "elide" }
doc.version; // "1.2"
doc.toString(); // "name: elide\n"
Streaming
stream() returns an async iterator that yields one document per step:
import { stream } from "elide:yaml";
for await (const record of stream("records.yaml")) {
console.log(record.id);
}
Documents are cut at --- and ... lines and parsed as the loop reaches them, so a file of many
documents never needs to be in memory at once. maxDocumentBytes (default 1 MiB) bounds one
document. Breaking out of the loop, await reader.close() and await using reader = stream(...)
all stop reading and release the source. Bytes passed to stream() are copied first.
Writing
stringify(value, options?) writes block-style YAML ending in a newline. Objects serialize as
JSON.stringify would (own enumerable keys, toJSON(), undefined and functions skipped in
objects and written as null in arrays), plus Map as a mapping, Set as a sequence, Date as a
timestamp and byte views as !!binary. Strings that would read back as something else ("123",
"true", "a: b") are quoted, and multi-line strings become block scalars. Repeated objects become
anchors and aliases. Cycles throw a TypeError with code ERR_INVALID_ARG_VALUE, regardless of
noRefs. stringifyAll(documents, options?)
separates documents with --- lines.
import { stringify } from "elide:yaml";
stringify({ name: "ada", tags: ["x", "y"], note: "line one\nline two" });
// "name: ada\ntags:\n - x\n - y\nnote: |-\n line one\n line two\n"
stringify({ a: [1, 2] }, { flowLevel: 0 }); // "{ a: [1, 2] }\n"
| Option | Default | Meaning |
|---|---|---|
indent | 2 | Spaces per level, 1 to 8 |
flowLevel | -1 | Depth from which collections use flow style; 0 for all, -1 for none |
sortKeys | false | Sort mapping keys |
noRefs | false | Write repeated objects inline |
quotingType | '"' | "'" quotes with single quotes |
forceQuotes | false | Quote every string |
Errors
Failures throw errors with a code; parse errors also carry 1-based line and column properties
and name the position in their message.
| Code | Error | Cause |
|---|---|---|
ERR_YAML_SYNTAX | SyntaxError | Malformed YAML, a duplicate key, a rejected alias or tag, or several documents where one was expected |
ERR_YAML_LIMIT | Error | A limits bound or maxDocumentBytes was exceeded |
ERR_YAML_CLOSED | Error | A closed stream was read |
ERR_YAML_IO | Error | The source failed while reading |
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE | TypeError | An invalid argument, option or value to write |
ERR_OUT_OF_RANGE | RangeError | A numeric option out of range |
File errors (ENOENT, EACCES, EISDIR) match node:fs.
TypeScript
@elide-dev/types declares the module. Name the shape you expect with a type argument:
import { parse, stream } from "elide:yaml";
interface Config {
port: number;
hosts: string[];
}
const config = parse<Config>(text);
for await (const record of stream<{ id: string }>("records.yaml")) {
record.id;
}
With elide:csv
Parsed documents are plain data, so elide:csv writes a list of mappings directly:
// to-csv.mjs
import { parseFile } from "elide:yaml";
import { stringify } from "elide:csv";
const { users } = parseFile("team.yaml");
process.stdout.write(stringify(users, { recordSeparator: "\n" }));
elide run to-csv.mjs prints:
id,name
1,ada
2,lin