@effected/yaml 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/YamlFormat.js CHANGED
@@ -238,8 +238,56 @@ function formatStream(text, documents, streamErrors, options) {
238
238
  }
239
239
  return parts.join("");
240
240
  }
241
- /** Convert a plain JS scalar value into a synthetic `YamlScalar` (offset/length are irrelevant — immediately re-stringified). */
242
- function jsValueToNode(value) {
241
+ /**
242
+ * Lower a plain JavaScript value into synthetic AST nodes (offset/length are
243
+ * irrelevant — the result is immediately re-stringified).
244
+ *
245
+ * Recursive by design: an array becomes a block {@link YamlSeq} and any other
246
+ * non-null object a block {@link YamlMap} over its own enumerable string keys,
247
+ * matching what `Yaml.stringify` does with the same value on the value path.
248
+ * Before this was recursive, a mapping or sequence value fell through to the
249
+ * node stringifier's `String(value)` fallback and landed in the document as
250
+ * the literal text `[object Object]` — a silent corruption that only surfaced
251
+ * on the next read (#642).
252
+ *
253
+ * The two conditions with no finite rendering fail typed rather than hanging
254
+ * or overflowing the stack: a cycle raises `CircularReference` and a graph
255
+ * deeper than `MAX_NESTING_DEPTH` raises `NestingDepthExceeded`. `seen` holds
256
+ * the ancestors of the value being lowered, so a value repeated across
257
+ * siblings (a shared, acyclic sub-object) is lowered twice rather than
258
+ * rejected.
259
+ */
260
+ function jsValueToNode(value, seen, depth) {
261
+ if (depth > 256) throw new ModifyFailure("NestingDepthExceeded", `Replacement value nests deeper than the maximum of ${256}`, 0, 0);
262
+ if (typeof value === "object" && value !== null) {
263
+ if (seen.has(value)) throw new ModifyFailure("CircularReference", "Replacement value contains a circular reference", 0, 0);
264
+ seen.add(value);
265
+ try {
266
+ if (Array.isArray(value)) return YamlSeq.make({
267
+ items: value.map((item) => jsValueToNode(item, seen, depth + 1)),
268
+ style: "block",
269
+ offset: 0,
270
+ length: 0
271
+ });
272
+ const record = value;
273
+ return YamlMap.make({
274
+ items: Object.keys(record).map((key) => YamlPair.make({
275
+ key: YamlScalar.make({
276
+ value: key,
277
+ style: "plain",
278
+ offset: 0,
279
+ length: 0
280
+ }),
281
+ value: jsValueToNode(record[key], seen, depth + 1)
282
+ })),
283
+ style: "block",
284
+ offset: 0,
285
+ length: 0
286
+ });
287
+ } finally {
288
+ seen.delete(value);
289
+ }
290
+ }
243
291
  return YamlScalar.make({
244
292
  value,
245
293
  style: "plain",
@@ -247,8 +295,12 @@ function jsValueToNode(value) {
247
295
  length: 0
248
296
  });
249
297
  }
298
+ /** Entry point for {@link jsValueToNode}: one `seen` set per lowering. */
299
+ function lowerValue(value) {
300
+ return jsValueToNode(value, /* @__PURE__ */ new Set(), 0);
301
+ }
250
302
  function modifyDocument(doc, path, value) {
251
- if (path.length === 0) return value === void 0 ? null : jsValueToNode(value);
303
+ if (path.length === 0) return value === void 0 ? null : lowerValue(value);
252
304
  if (doc.contents === null) throw new ModifyFailure("EmptyDocument", "Cannot navigate path in empty document", 0, 0);
253
305
  return modifyNode(doc.contents, path, 0, value);
254
306
  }
@@ -264,7 +316,7 @@ function modifyNode(node, path, depth, value) {
264
316
  newItems.splice(pairIndex, 1);
265
317
  return rebuildMap(node, newItems);
266
318
  }
267
- const newValueNode = jsValueToNode(value);
319
+ const newValueNode = lowerValue(value);
268
320
  if (pairIndex >= 0) {
269
321
  const newItems = [...node.items];
270
322
  const oldPair = newItems[pairIndex];
@@ -304,8 +356,8 @@ function modifyNode(node, path, depth, value) {
304
356
  const newItems = [...node.items];
305
357
  if (value === void 0) {
306
358
  if (idx < newItems.length) newItems.splice(idx, 1);
307
- } else if (idx < newItems.length) newItems[idx] = jsValueToNode(value);
308
- else newItems.push(jsValueToNode(value));
359
+ } else if (idx < newItems.length) newItems[idx] = lowerValue(value);
360
+ else newItems.push(lowerValue(value));
309
361
  return rebuildSeq(node, newItems);
310
362
  }
311
363
  if (idx >= node.items.length) throw new ModifyFailure("InvalidIndex", `Index ${idx} out of bounds`, node.offset, node.length);
@@ -456,12 +508,27 @@ var YamlFormat = class YamlFormat {
456
508
  /**
457
509
  * Compute the edits that insert, replace, or remove a value at `path`.
458
510
  * Passing `value === undefined` removes the target key/element; a missing
459
- * insertion target appends after the last pair/element. Only
460
- * scalar-compatible values are supported (matching v3 — arbitrary object
461
- * graphs are not recursively lowered into AST nodes). Fails with
511
+ * insertion target appends after the last pair/element. Fails with
462
512
  * {@link YamlModificationError} on a fatal parse error or a structural
463
513
  * navigation mismatch.
464
514
  *
515
+ * **`value` may be a whole object graph.** An array is written as a block
516
+ * sequence and any other non-null object as a block mapping over its own
517
+ * enumerable string keys, recursively — the same lowering
518
+ * {@link Yaml.stringify} applies to the same value, so `modify` and
519
+ * `stringify` agree on what a given JavaScript value means. (Before
520
+ * 0.14.0 only scalars were lowered and a mapping or sequence value was
521
+ * coerced through `String(value)`, writing the literal text
522
+ * `[object Object]` into the document — see #642.) Only the surrounding
523
+ * document is preserved byte-for-byte; the replacement subtree is
524
+ * synthesized, so it carries no comments and takes the stringifier's
525
+ * styles.
526
+ *
527
+ * Two replacement values have no finite rendering and fail typed rather
528
+ * than hanging or overflowing the stack: one containing a circular
529
+ * reference (`CircularReference`) and one nesting deeper than 256 levels
530
+ * (`NestingDepthExceeded`).
531
+ *
465
532
  * **Single-document contract.** A `path` carries no document index, so on
466
533
  * a multi-document stream there is no rule for which document it names —
467
534
  * `modify` fails with {@link YamlModificationError} carrying a
package/index.d.ts CHANGED
@@ -54,7 +54,7 @@ export type YamlStringifyErrorCode = typeof YamlStringifyErrorCode.Type;
54
54
  *
55
55
  * @public
56
56
  */
57
- export declare const YamlModifyErrorCode: Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument"]>;
57
+ export declare const YamlModifyErrorCode: Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>;
58
58
  /**
59
59
  * The union of all modify-stage error code string literals.
60
60
  *
@@ -67,7 +67,7 @@ export type YamlModifyErrorCode = typeof YamlModifyErrorCode.Type;
67
67
  *
68
68
  * @public
69
69
  */
70
- export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument"]>]>;
70
+ export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>]>;
71
71
  /**
72
72
  * The union of all YAML error code string literals.
73
73
  *
@@ -75,7 +75,7 @@ export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<reado
75
75
  */
76
76
  export type YamlErrorCode = typeof YamlErrorCode.Type;
77
77
  declare const YamlDiagnostic_base: Schema.Class<YamlDiagnostic, Schema.Struct<{
78
- readonly code: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument"]>]>;
78
+ readonly code: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>]>;
79
79
  readonly message: Schema.String;
80
80
  readonly offset: Schema.Number;
81
81
  readonly length: Schema.Number;
@@ -1219,12 +1219,27 @@ export declare class YamlFormat {
1219
1219
  /**
1220
1220
  * Compute the edits that insert, replace, or remove a value at `path`.
1221
1221
  * Passing `value === undefined` removes the target key/element; a missing
1222
- * insertion target appends after the last pair/element. Only
1223
- * scalar-compatible values are supported (matching v3 — arbitrary object
1224
- * graphs are not recursively lowered into AST nodes). Fails with
1222
+ * insertion target appends after the last pair/element. Fails with
1225
1223
  * {@link YamlModificationError} on a fatal parse error or a structural
1226
1224
  * navigation mismatch.
1227
1225
  *
1226
+ * **`value` may be a whole object graph.** An array is written as a block
1227
+ * sequence and any other non-null object as a block mapping over its own
1228
+ * enumerable string keys, recursively — the same lowering
1229
+ * {@link Yaml.stringify} applies to the same value, so `modify` and
1230
+ * `stringify` agree on what a given JavaScript value means. (Before
1231
+ * 0.14.0 only scalars were lowered and a mapping or sequence value was
1232
+ * coerced through `String(value)`, writing the literal text
1233
+ * `[object Object]` into the document — see #642.) Only the surrounding
1234
+ * document is preserved byte-for-byte; the replacement subtree is
1235
+ * synthesized, so it carries no comments and takes the stringifier's
1236
+ * styles.
1237
+ *
1238
+ * Two replacement values have no finite rendering and fail typed rather
1239
+ * than hanging or overflowing the stack: one containing a circular
1240
+ * reference (`CircularReference`) and one nesting deeper than 256 levels
1241
+ * (`NestingDepthExceeded`).
1242
+ *
1228
1243
  * **Single-document contract.** A `path` carries no document index, so on
1229
1244
  * a multi-document stream there is no rule for which document it names —
1230
1245
  * `modify` fails with {@link YamlModificationError} carrying a
@@ -40,7 +40,7 @@ function composeBlockMapInner(blockMapCst, state, externalFirstKey, meta) {
40
40
  if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
41
41
  checkMultilineImplicitKeys(pairs, state);
42
42
  const offset = externalFirstKey ? "offset" in externalFirstKey ? externalFirstKey.offset : blockMapCst.offset : blockMapCst.offset;
43
- const length = blockMapCst.offset + blockMapCst.length - offset;
43
+ const length = trimDisownedTrailingComments(state.text, offset, blockMapCst.offset + blockMapCst.length, state.escapedComments) - offset;
44
44
  const mapComment = meta?.comment !== void 0 ? trailingComment !== void 0 ? `${meta.comment}\n${trailingComment}` : meta.comment : trailingComment;
45
45
  const map = new YamlMap({
46
46
  items: pairs,
@@ -54,6 +54,49 @@ function composeBlockMapInner(blockMapCst, state, externalFirstKey, meta) {
54
54
  if (meta?.anchor) registerAnchor(map, meta.anchor, state, offset);
55
55
  return map;
56
56
  }
57
+ /**
58
+ * Narrow a block collection's raw CST span so it stops before any trailing
59
+ * comment line the collection's own comment model has already DISOWNED.
60
+ *
61
+ * The CST length of a block map/seq runs to the end of the last physical line
62
+ * the parser consumed, which includes a floating comment sitting at a column
63
+ * shallower than the collection's content. The comment model deliberately
64
+ * does not give that comment to the collection — `buildPairs` and the block-seq
65
+ * terminal partition push it onto `state.escapedComments`, where the enclosing
66
+ * scope re-attributes it (see `EscapedComment` in `comments.ts`: "it belongs to
67
+ * an outer scope"). The span was the one place that disagreed, reporting an
68
+ * `offset`/`length` whose end lay past a comment the node does not own — so a
69
+ * caller splicing text at the end of the collection inserted AFTER the comment
70
+ * (#642's sibling, #643).
71
+ *
72
+ * The disowned set is the input rather than a re-derived column test, so the
73
+ * span and the comment model cannot drift apart: whatever the partition threw
74
+ * out, the span excludes.
75
+ *
76
+ * Blank lines immediately above a trimmed run go with the run, not with the
77
+ * last entry — but only when something was trimmed, so a collection whose span
78
+ * merely ends in blank lines keeps the end it has always reported.
79
+ *
80
+ * Composition is bottom-up, so a nested collection's own span has already been
81
+ * trimmed by the time its parent's items are read.
82
+ */
83
+ function trimDisownedTrailingComments(text, offset, end, disowned) {
84
+ let cut = end;
85
+ for (const ec of disowned) {
86
+ if (ec.offset < offset || ec.offset >= cut) continue;
87
+ const lineStart = text.lastIndexOf("\n", ec.offset - 1) + 1;
88
+ if (lineStart <= offset || lineStart >= cut) continue;
89
+ cut = lineStart;
90
+ }
91
+ if (cut === end) return end;
92
+ while (cut > offset && text[cut - 1] === "\n") {
93
+ const prevLineStart = text.lastIndexOf("\n", cut - 2) + 1;
94
+ if (prevLineStart <= offset) break;
95
+ if (text.slice(prevLineStart, cut - 1).trim() !== "") break;
96
+ cut = prevLineStart;
97
+ }
98
+ return cut;
99
+ }
57
100
  function flattenBlockMapChildren(children, state, externalKeyColumn, externalKeyOffset) {
58
101
  const items = [];
59
102
  let pendingMeta = {};
@@ -1344,11 +1387,12 @@ function composeBlockSeqInner(cst, state, meta) {
1344
1387
  } else seqTrailing = withLeadingBlank(pending);
1345
1388
  }
1346
1389
  const seqComment = meta?.comment !== void 0 ? seqTrailing !== void 0 ? `${meta.comment}\n${seqTrailing}` : meta.comment : seqTrailing;
1390
+ const seqEnd = trimDisownedTrailingComments(state.text, cst.offset, cst.offset + cst.length, state.escapedComments);
1347
1391
  const seq = new YamlSeq({
1348
1392
  items: rawItems,
1349
1393
  style: "block",
1350
1394
  offset: cst.offset,
1351
- length: cst.length,
1395
+ length: seqEnd - cst.offset,
1352
1396
  ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
1353
1397
  ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
1354
1398
  ...seqComment !== void 0 ? { comment: seqComment } : {}
@@ -1371,7 +1415,7 @@ function composeFlatBlockMap(children, startIdx, parentCst, state, externalFirst
1371
1415
  if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
1372
1416
  checkMultilineImplicitKeys(pairs, state);
1373
1417
  const offset = "offset" in externalFirstKey ? externalFirstKey.offset : parentCst.offset;
1374
- const end = parentCst.offset + parentCst.length;
1418
+ const end = trimDisownedTrailingComments(state.text, offset, parentCst.offset + parentCst.length, state.escapedComments);
1375
1419
  const mapComment = meta?.comment !== void 0 ? trailingComment !== void 0 ? `${meta.comment}\n${trailingComment}` : meta.comment : trailingComment;
1376
1420
  const map = new YamlMap({
1377
1421
  items: pairs,
@@ -52,6 +52,13 @@ const YAML_STRINGIFY_ERROR_CODES = ["CircularReference"];
52
52
  * not re-emit `%YAML`/`%TAG` directive lines, and re-emitting a document
53
53
  * without its `%TAG` orphans every shorthand tag that depends on it — the
54
54
  * output would be unparseable — so modify fails typed rather than corrupting.
55
+ *
56
+ * `CircularReference` and `NestingDepthExceeded` are the two limits on the
57
+ * replacement value itself: `modify` lowers a plain JavaScript value into
58
+ * AST nodes, and an object graph that points back at itself or nests deeper
59
+ * than `MAX_NESTING_DEPTH` has no finite YAML rendering. Both names are
60
+ * deliberately the ones the stringify and compose stages already use for the
61
+ * same conditions, so `YamlErrorCode` gains no member.
55
62
  */
56
63
  const YAML_MODIFY_ERROR_CODES = [
57
64
  "EmptyDocument",
@@ -59,7 +66,9 @@ const YAML_MODIFY_ERROR_CODES = [
59
66
  "InvalidIndex",
60
67
  "NotNavigable",
61
68
  "MultiDocumentStream",
62
- "DirectiveCarryingDocument"
69
+ "DirectiveCarryingDocument",
70
+ "CircularReference",
71
+ "NestingDepthExceeded"
63
72
  ];
64
73
  /**
65
74
  * The single source of truth for which diagnostic codes are fatal to a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "private": false,
5
5
  "description": "Zero-dependency YAML parsing, editing, formatting and linting as Effect schemas.",
6
6
  "keywords": [