Esc

tinyaml

Switching from another library

Change one import to move from js-yaml, yaml, yamljs or @std/yaml, and what each drop-in does differently.

Each subpath exports the functions, options and types of the library it replaces, apart from the gaps listed below, so switching is a change to the import line. The sizes are esbuild bundles of the published files, minified, then gzip -9. The last column is that library's own test suite, run with tinyaml in its place.

Library Change the import to Size, gzip Its own tests on tinyaml
js-yaml 4.3.2 "tinyaml/js-yaml" 14.4 → 11.7 KiB 301 of 304 pass
js-yaml 5.5.0 "tinyaml/js-yaml-v5" 17.1 → 16.0 KiB 1,023 of 1,646 pass
yaml 2.9.1 "tinyaml/yaml" 29.9 → 16.8 KiB 195 of 220 pass
yamljs 0.3.0 "tinyaml/yamljs" 8.8 → 8.0 KiB 103 of 103 pass
@std/yaml 1.3.0 "tinyaml/std" 11.3 → 10.7 KiB 92 of 95 pass
// before
import yaml from "js-yaml";
// after
import yaml from "tinyaml/js-yaml";
ts

Of js-yaml 5's 1,646 tests, 610 call the events and AST API that tinyaml/js-yaml-v5 leaves out, and js-yaml 5 skips 13 itself. The other 1,023 pass. yaml's five test files that cover parse and stringify hold 531 tests, and the 220 counted are the ones that call tinyaml. The rest test yaml's Document API.

For yamljs and @std/yaml the swap saves less than 1 KiB. The bigger change is the parser. On the yaml-test-suite, tinyaml/yamljs loads 144 cases that yamljs throws on, and in 39 where the two disagree only tinyaml/yamljs matches the suite.

Every subpath runs the same parser code as the root. The details below list each known difference from the original.

  • The listener option isn't supported. It accounts for 2 of the 3 tests that don't pass. js-yaml skips the third test itself.
  • safeLoad, safeLoadAll and safeDump work and do what load, loadAll and dump do. js-yaml 4 throws for them.
  • js-yaml's tests make 746 calls to load, loadAll and dump. Replayed through tinyaml, 740 give the same result, and 6 throw the same error at a different position. tinyaml throws in every case js-yaml does.
  • On the yaml-test-suite, 399 of 402 cases give the same result as js-yaml. 1 is valid YAML that tinyaml loads and js-yaml rejects (9KAX). The other 2 are js-yaml bugs tinyaml doesn't copy: js-yaml drops a tag on its own line above a less indented block scalar (M5C3), and it loads the invalid H7J7.
  • 5,000 randomly edited suite inputs: none load to a different value, none throw only in tinyaml, and 10 load where js-yaml throws, 1 of them invalid YAML. 5,000 random values give matching outcomes, including matching errors: 4,021 dump to the same text and 979 throw the same error.
  • Only the load and dump API: load, loadAll, dump, YAMLException, Schema, the schemas, the tag definitions and the style constants. The events, AST, presenter and visit API isn't there: parseEvents, getScalarValue, constructFromEvents, eventsToAst, jsToAst, present, visit, and the EVENT_*, VISIT_* and CHOMPING_* constants.
  • dump's transform option works. dump builds the same AST as js-yaml 5 and passes it to transform, but there is no visit to walk it with.
  • js-yaml 5's tests make 10,085 calls to load, loadAll and dump, 5,256 of them different in function, input or options. Replayed through tinyaml, 5,213 of those 5,256 give the same result. 20 throw the same error at a different position, and 22 throw with a different message. tinyaml throws in every case js-yaml 5 does.
  • The one call that loads to a different value is an empty key tagged !!merge with mergeTag in the schema, ? !!merge\n: []. js-yaml 5 loads {}, and tinyaml loads { "<<": [] }.
  • A custom mapping tag gets js-yaml 5's has, addPair and get calls for each node, in the same order, but tinyaml builds a node when it ends. So an outer node's create comes after the nodes inside it, a merge calls its source's keys before create for the node it merges into, and a repeated key throws before the node's earlier pairs are added.
  • When the schema has a mapping tag other than the object maps, a repeated symbol or function key in an untagged mapping throws with the start of the input as its position, since tinyaml builds that mapping after reading the whole stream.
  • On the yaml-test-suite, all 402 cases give the same result as js-yaml 5. Both reject 126, and 109 of those throw the same message.
  • 5,000 randomly edited suite inputs: none load to a different value, and 2 load where js-yaml 5 throws. 5,000 random values give matching outcomes, including matching errors: 3,584 dump to the same text and 1,416 throw the same error.
  • Only parse and stringify. There is no parseDocument, parseAllDocuments, Document, CST, Lexer, Parser, Composer, LineCounter or visitor API.
  • parse doesn't support mapAsMap, stringKeys, strict: false (which strict: undefined also means to yaml), or recovering from errors with logLevel: "silent". Collection keys are turned into strings that don't always match yaml's.
  • A lone carriage return is a line break, as the YAML spec has it. yaml reads it as text, so - a\r- b is ["a", "b"] where yaml gives ["a\r- b"]. Error positions count such input as one line, as yaml's do.
  • stringify doesn't take Document or Node values, writes no comments, and doesn't use customTags or the YAML 1.1 !!set, !!omap and !!binary tags.
  • A collection tag's resolve gets a stand-in for yaml's YAMLMap or YAMLSeq node. It has items, with Scalar, Pair and collection nodes in them, get, has, toJSON and toString, and nothing else of yaml's node API. An alias in it is a node of the alias's value, toJSON returns the collection as tinyaml parsed it, and nodeClass isn't used. Its scalars have no tag, so !!set takes a value tagged !!null as null, where yaml rejects it. When resolve returns a node from inside the collection, onAnchor still hears of the anchors elsewhere in it. When it returns something else, a later alias to an anchor in the collection reads the anchor's value, where yaml throws.
  • A << that merges a !!set merges nothing. yaml splits each member into a key and a value, so {<<: !!set {abc}} gives { a: "b" }, and throws a TypeError on a member it can't split, like a number.
  • With compat, stringify quotes what yaml quotes and parse warns where yaml warns about plain scalars, but parse doesn't warn about a flow collection whose closing bracket is at its parent's indentation.
  • The upstream run covers yaml's tests of parse and stringify, the stringify, parse, errors, types and anchors files in tests/doc/. Of the 25 that fail, 4 are logLevel tests that fail against yaml itself under Vitest, and 13 use the features above. 5 stringify with indent: 1, indent: 4 or indentSeq: false, where the output differs. The last 3 are parse cases: a merge key missing its space, which throws another message, carriage returns inside double quotes, and a collection key in an alias expansion test with the limits turned off.
  • Across 7 option sets on the yaml-test-suite, 2,593 of 2,814 runs agree with yaml.
  • 5,000 randomly edited suite inputs: 1,943 give the same value and the same warnings. 21 more throw the same error with other warnings, since tinyaml stops at the first error and yaml warns about what follows it, unknown aliases included. 2,923 throw in both: 353 the same error, 763 another error code, and the rest the same code at another position or with another message, since tinyaml marks one character where yaml marks the whole token, and words most errors as js-yaml 4 does. 36 load to a different value, 20 of those with a collection key. 47 load only in tinyaml and 30 only in yaml. Lone carriage returns, which yaml reads as text, cause most of these: all 16 other values without a collection key, 39 of the 47 and 22 of the 30. 5,000 random values: 4,932 stringify to the same text, 68 throw the same error, none differ.

stringify writes what yamljs writes, byte for byte. parse gives yamljs's values but reads structure as YAML does where yamljs's result is broken:

  • Multi-line plain and quoted scalars fold line breaks to spaces, and folded block scalars keep more-indented lines. a: x\n y is "x y", where yamljs gives "x\ny".
  • - - a is [["a"]], where yamljs gives ["- a"]. - ? a is [{ a: null }], where yamljs gives ["? a"].
  • Flow mapping keys are read whole: {a b: 1} is { "a b": 1 }, where yamljs gives { a: "b: 1" }.
  • \x, \u and \U escapes give the character, not its UTF-8 bytes.
  • Block scalars keep trailing spaces on their last line.
  • Quoted strings stay strings. ['*a'] is ["*a"] and !!str "x" is "x", where yamljs gives [undefined] and '"x"'.
  • An alias in a flow collection gives its anchor's value. After a: &x 1, b: {c: *x} is { c: 1 } and b: [*x] is [1], where yamljs gives the text "*x".
  • Keys named like built-in object properties, such as constructor, toString or hasOwnProperty, are kept. yamljs drops them: constructor: 1 gives null and {constructor: 1} gives {}.
  • << merges in flow mappings too. yamljs merges only in block mappings, so {<<: {a: 1}} gives { "<<": { a: 1 } } there.
  • A merge doesn't replace a key already set to null. After x: &x {a: 1}, y:\n a: ~\n <<: *x keeps a: null, where yamljs gives a: 1.
  • A quoted "<<" or '<<' is a key and doesn't merge, where yamljs merges it.
  • << merges mappings only. yamljs also merges a sequence by index: after x: &x [1, 2], <<: *x copies 0: 1 and 1: 2, where tinyaml throws.
  • A comment line is never read as a value or key.
  • Errors are ParseExceptions with the root's messages.

On the yaml-test-suite's 402 cases, 186 give the same result as yamljs, and 144 load where yamljs throws. In 39 the values differ and only tinyaml/yamljs matches the suite. In 33 neither matches or the case is invalid YAML. There is no case where only yamljs is right. 5,000 random values give matching outcomes, including matching errors: 4,994 dump to the same text and 6 throw the same error.

  • unstable_parse and unstable_stringify aren't supported. They account for 2 of the 3 tests that don't pass. The third starts a Deno subprocess, which can't run under Node.
  • On the yaml-test-suite, 360 of 402 cases give the same result as std, 99 of them the same error message. 11 parse where std throws. The other 31 differ, and in all 31 std's own result is wrong by the suite's answers.
  • 5,000 randomly edited suite inputs: 117 parse to a different value, 146 throw only in tinyaml, and 48 parse where std throws, so values or acceptance differ in 311 of them, 6.2%. 2,866 throw in both, 2,542 of them with the same message and 2,570 with the same message apart from the position.
  • 5,000 random values give matching outcomes, including matching errors: 3,499 stringify to the same text and 1,501 throw the same error.