xyaml
A secure, extensible YAML compiler for TypeScript, Bun, Node.js, and modern browsers.
xyaml adds deterministic references, composition, resource imports, typed results, and opt-in JavaScript evaluation to standard YAML. The default mode does not execute code or access the network.
Features
- Explicit
self,parent,root, andcontextreferences - Deterministic
$includemerging and typed!importvalues - Local files, HTTP resources, raw text, JSON, and JavaScript modules
- Safe-by-default parsing with a clearly separated trusted mode
- Async and sync Node.js APIs, plus a browser-safe core
- First-class TypeScript declarations, ESM, CommonJS, and a CLI
- Structured errors, source metadata, cycle detection, caching, and cancellation
- YAML 1.2 support powered by
yaml
Installation
bun add xyaml
npm install xyaml
xyaml requires Bun 1.3+ or Node.js 22+.
Quick start
# config.xyaml
$include:
- ./defaults.xyaml
app:
host: localhost
port: 8000
url: http://${self.host}:${self.port}
features: !import ./features.json
license: !text ./LICENSE
import { loadFile } from 'xyaml/node';
interface Config {
app: {
host: string;
port: number;
url: string;
};
features: Record<string, boolean>;
license: string;
}
const config = await loadFile<Config>('./config.xyaml');
The generic type describes the expected result; it is not runtime validation. Validate untrusted data with the schema library used by your application.
References
References are intentionally explicit:
name: xyaml
port: 8000
portCopy: ${self.port}
server:
host: localhost
url: http://${self.host}:${parent.port}
packageName: ${root.name}
environment: ${context.environment}
selfis the mapping or sequence containing the current value.parentis the parent ofself.rootis the compiled root document.contextis supplied by the caller.
An exact reference preserves its type. Embedded references accept scalar values and produce a
string. Use ${self.value} to emit the literal text ${self.value}. Missing references,
ambiguous short names, object interpolation, and cycles are errors.
import { parse } from 'xyaml';
const config = parse(source, {
context: { environment: 'production' },
});
Composition
$include
$include merges mappings into the current mapping:
$include:
- ./defaults.xyaml
- source: ./environments.xyaml
select: production
port: 9000
Includes are applied from left to right. Later includes override earlier includes, and local keys override every included key. Included values must be mappings.
!import
!import inserts a YAML, XYAML, or JSON resource as a value:
database: !import ./database.xyaml
enabledFeatures: !import
source: ./features.json
select: production
Local Node.js resolution checks the exact path, then .xyaml, .yaml, .yml, .json, and
matching index.* files.
!text and !resolve
license: !text ./LICENSE
absoluteConfigPath: !resolve ./config.xyaml
!text returns the resource without parsing it. !resolve resolves a specifier without reading
it; the Node loader returns a native absolute path for file: URLs.
YAML's standard explicit tags replace the coercion commands from xyaml 0.x:
portAsString: !!str 8000
portAsInteger: !!int "8000"
ratio: !!float "0.75"
Trusted mode
Trusted mode enables arbitrary JavaScript. It is intended only for configuration files controlled by the application owner.
math:
base: 2
add: !fn |
(a, b) => a + b + self.base
result: !expr self.add(3, 4)
$eval:
- document.delete("math.add")
const config = await loadFile('./config.xyaml', { trusted: true });
!exprevaluates a synchronous JavaScript expression.!fnevaluates a function expression and captures itsself,parent,root, andcontext.$evalexecutes synchronous statements after the complete document has been resolved.$evalalso receivesdocument.get,document.has,document.set, anddocument.delete.
Trusted mode uses the JavaScript runtime itself. It is not a sandbox and does not protect against filesystem access, process access, infinite loops, or global mutation.
JavaScript modules
The built-in Node loader supports local JavaScript modules in trusted mode:
generated: !module
source: ./generate.mjs
args: [production]
export default ({ args, context, source }) => ({
environment: args[0],
region: context.region,
loadedFrom: source.href,
});
Async loading supports .mjs, .cjs, and .js. loadFileSync only supports .cjs. Remote
JavaScript modules are not supported by the built-in loader.
Values containing functions cannot be serialized. Remove build-time helper functions with $eval
before passing the result to the CLI or stringify.
HTTP resources
HTTP access is disabled by default:
const config = await loadFile('https://example.com/config.xyaml', {
allowHttp: true,
});
The browser-safe core uses a fetch loader:
import { compile, createFetchLoader } from 'xyaml';
const baseUrl = new URL('/config/app.xyaml', window.location.href);
const loader = createFetchLoader({ baseUrl });
const source = await fetch(baseUrl).then(response => response.text());
const config = await compile(source, {
allowHttp: true,
baseUrl,
loader,
});
Pass an AbortSignal as signal to cancel asynchronous resource loading. A compiler caches each
canonical resource for the duration of one compilation.
API
Universal core
import {
compile,
compileDocument,
createCompiler,
parse,
parseDocument,
stringify,
} from 'xyaml';
parse<T>(source, options?)synchronously compiles a string. External resources require a loader with synchronous support.compile<T>(source, options?)asynchronously compiles a string with full resource support.parseDocument<T>andcompileDocument<T>return anXyamlDocument<T>.createCompiler(options?)creates a reusable compiler configuration. Caches remain isolated to each compilation.stringify(value, options?)serializes a non-cyclic value to YAML and rejects functions and symbols.
Node.js and Bun
import {
loadFile,
loadFileDocument,
loadFileDocumentSync,
loadFileSync,
} from 'xyaml/node';
loadFile<T>(pathOrUrl, options?)is the recommended complete API.loadFileSync<T>supports local YAML, JSON, text, and CommonJS modules.- Document variants return metadata and navigation methods.
Document API
const document = await loadFileDocument('./config.xyaml');
document.get('server.port');
document.has(['server', 'host']);
document.set('server.port', 9000);
document.delete('internal');
document.toJS();
document.toJSON();
document.toString();
toJS() and toJSON() return independent snapshots. Parse and resource errors throw
XyamlError, which includes a stable code and optional source, path, and cause.
Custom resource loaders
Implement ResourceLoader to integrate virtual filesystems, application caches, authenticated
HTTP clients, or browser storage:
import type { ResourceLoader, ResourceSource } from 'xyaml';
class VirtualLoader implements ResourceLoader {
resolve(specifier: string, importer?: URL): URL {
return new URL(specifier, importer);
}
async load(url: URL): Promise<ResourceSource> {
return { url, content: await readFromApplicationStorage(url) };
}
}
loadSync, loadModule, loadModuleSync, and display are optional capabilities.
CLI
xyaml compile config.xyaml
xyaml compile config.xyaml --output config.json
xyaml compile config.xyaml --format yaml --output compiled.yaml
xyaml compile - --base ./config --context context.yaml
Options:
-o, --output <file>writes to a file instead of stdout.-f, --format json|yamlselects the output format.--context <file>loads a YAML or JSON mapping ascontext.--base <path|url>sets the base for stdin imports.--trustedenables executable features.--allow-httpenables HTTP(S) resources.
JSON is the default stdout format. A .yaml, .yml, or .xyaml output filename selects YAML when
--format is omitted. Diagnostics are written to stderr and failures use a non-zero exit code.
Migration from 0.x
Version 1.0 intentionally replaces the historical string-command grammar:
| xyaml 0.x | xyaml 1.0 |
|---|---|
${host} |
${self.host} |
~include |
$include |
~import |
!import |
~read, ~download |
!text |
~path, ~resolve |
!resolve |
~str, ~num |
!!str, !!int, !!float |
~ex or JavaScript interpolation |
!expr with trusted: true |
Inline fn or arrow functions |
!fn with trusted: true |
eval |
$eval with trusted: true |
JS modules mutating process.argv |
!module with { args, context, source } |
dump |
stringify |
The old releases remain useful as historical references, but 1.0 does not include a compatibility parser.
Development
bun install
bun run check
The complete check runs formatting, linting, strict TypeScript, Bun tests with coverage, the
library build, Node ESM/CommonJS tests, a browser-target bundle smoke test, publint, and Are The
Types Wrong.