@pnpm/yaml.document-sync 1100.0.0 → 1100.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @pnpm/yaml.document-sync
2
+
3
+ ## 1100.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - pnpm now preserves scalar YAML anchors and aliases when editing `pnpm-workspace.yaml`. Removing the entry that defines an anchor keeps surviving aliases valid. Entries updated to different values are written separately [#8245](https://github.com/pnpm/pnpm/issues/8245).
8
+
9
+ - pnpm now preserves comments and existing key order when updating `package.yaml`. New keys are appended to their mapping [pnpm/pnpm#2008](https://github.com/pnpm/pnpm/issues/2008).
10
+
11
+ ## 1100.0.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug [#13164](https://github.com/pnpm/pnpm/issues/13164).
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Update a YAML document to match the contents of an in-memory object.
4
4
 
5
5
  <!--@shields('npm')-->
6
- [![npm version](https://img.shields.io/npm/v/@pnpm/yaml.document-sync.svg)](https://www.npmjs.com/package/@pnpm/yaml.document-sync)
6
+ [![npm version](https://img.shields.io/npm/v/@pnpm/yaml.document-sync.svg)](https://npmx.dev/package/@pnpm/yaml.document-sync)
7
7
  <!--/@-->
8
8
 
9
9
  ## Installation
@@ -60,11 +60,25 @@ qux:
60
60
  - 3
61
61
  ```
62
62
 
63
+ For package manifests, retain null values and empty maps and keep existing keys in their original order:
64
+
65
+ ```ts
66
+ patchDocument(document, target, {
67
+ preserveKeyOrder: true,
68
+ preserveScalarAliases: true,
69
+ pruneEmptyValues: false,
70
+ })
71
+ ```
72
+
73
+ New keys are appended to their mapping. By default, keys follow the target object's order, and null values and empty maps are pruned from existing nodes.
74
+
75
+ Consumers that use a different conversion for scalar mapping keys can pass `stringifyKey`. For example, `stringifyKey: String` matches `js-yaml`, which exposes a null mapping key as the property name `"null"`. The default follows `yaml` and uses an empty string.
76
+
63
77
  ## Purpose
64
78
 
65
79
  This package is useful when your codebase:
66
80
 
67
- 1. Uses the [yaml](https://www.npmjs.com/package/yaml) library.
81
+ 1. Uses the [yaml](https://npmx.dev/package/yaml) library.
68
82
  2. Calls `.toJSON()` on the parse result and performs changes to it.
69
83
  3. Needs to "sync" those changes back to the source document.
70
84
 
@@ -164,6 +178,16 @@ bar:
164
178
  - 3
165
179
  ```
166
180
 
181
+ Set `preserveScalarAliases: true` to retain scalar aliases whose final values
182
+ agree. If an anchor's defining entry is removed, the first surviving entry
183
+ defines it. An entry whose value differs from the defining entry is written
184
+ as a scalar. This option does not coordinate dependency updates; it preserves
185
+ YAML representation while applying the supplied target values.
186
+
187
+ ```ts
188
+ patchDocument(document, target, { preserveScalarAliases: true })
189
+ ```
190
+
167
191
  ## License
168
192
 
169
193
  MIT
@@ -1,5 +1,11 @@
1
1
  import yaml from 'yaml';
2
2
  export interface PatchDocumentOptions {
3
+ /** Convert scalar keys to target property names. Defaults to YAML's null-to-empty-string conversion. */
4
+ readonly stringifyKey?: (key: unknown) => string;
5
+ /** Keep existing map keys in their original order and append new keys. */
6
+ readonly preserveKeyOrder?: boolean;
7
+ /** Remove null values and empty maps. Defaults to true for configuration files. */
8
+ readonly pruneEmptyValues?: boolean;
3
9
  /**
4
10
  * Updating aliases is inherently ambiguous since they're not a concept in
5
11
  * JSON. The default is to unwrap and remove aliases since that's the most
@@ -12,6 +18,13 @@ export interface PatchDocumentOptions {
12
18
  * @default 'unwrap'
13
19
  */
14
20
  readonly aliases?: 'unwrap' | 'follow';
21
+ /**
22
+ * Keep scalar aliases whose final values agree, moving removed anchors to a
23
+ * surviving entry. Divergent values become independent scalars. When enabled,
24
+ * this takes precedence over `aliases` for scalar nodes only.
25
+ * @default false
26
+ */
27
+ readonly preserveScalarAliases?: boolean;
15
28
  }
16
29
  /**
17
30
  * Recursively update a YAML document (in-place) to match the contents of a
@@ -1,4 +1,5 @@
1
1
  import yaml from 'yaml';
2
+ import { preserveScalarAliases } from './preserveScalarAliases.js';
2
3
  /**
3
4
  * Recursively update a YAML document (in-place) to match the contents of a
4
5
  * target value.
@@ -11,17 +12,22 @@ export function patchDocument(document, target, options) {
11
12
  if (document.errors.length > 0) {
12
13
  throw new Error('Document with errors cannot be patched');
13
14
  }
15
+ const restoreAliases = options?.preserveScalarAliases ? preserveScalarAliases(document) : undefined;
14
16
  document.contents = patchNode(document.contents, target, {
17
+ ...options,
15
18
  document,
16
19
  aliases: options?.aliases ?? 'unwrap',
17
20
  });
21
+ restoreAliases?.();
18
22
  }
19
23
  function patchNode(node, target, ctx) {
20
24
  if (node == null) {
21
25
  return ctx.document.createNode(target);
22
26
  }
23
27
  if (target == null) {
24
- return null;
28
+ if (ctx.pruneEmptyValues !== false)
29
+ return null;
30
+ return yaml.isScalar(node) && node.value === target ? node : ctx.document.createNode(target);
25
31
  }
26
32
  if (yaml.isAlias(node)) {
27
33
  return patchAlias(node, target, ctx);
@@ -56,8 +62,7 @@ function patchAlias(alias, target, ctx) {
56
62
  case 'unwrap': {
57
63
  const copy = resolved.clone();
58
64
  copy.anchor = undefined;
59
- patchNode(copy, target, ctx);
60
- return copy;
65
+ return patchNode(copy, target, ctx);
61
66
  }
62
67
  }
63
68
  }
@@ -65,37 +70,42 @@ function patchScalar(scalar, target, ctx) {
65
70
  if (scalar.value === target) {
66
71
  return scalar;
67
72
  }
68
- if (typeof target === 'boolean' || typeof target === 'string' || typeof target === 'number') {
69
- scalar.value = target;
73
+ const replacement = ctx.document.createNode(target);
74
+ if (yaml.isScalar(replacement)) {
75
+ scalar.value = replacement.value;
76
+ scalar.tag = replacement.tag;
70
77
  return scalar;
71
78
  }
72
- return ctx.document.createNode(target);
79
+ return replacement;
73
80
  }
74
81
  function patchMap(map, target, ctx) {
75
82
  if (!isRecord(target)) {
76
83
  return ctx.document.createNode(target);
77
84
  }
78
- // Intentionally return null on empty maps as well. This recursively clears
79
- // empty maps in the final document.
80
- if (target == null || Object.keys(target).length === 0) {
85
+ if (ctx.pruneEmptyValues !== false && Object.keys(target).length === 0) {
81
86
  return null;
82
87
  }
83
88
  const mapKeyToExistingPair = new Map();
84
89
  for (const pair of map.items) {
85
90
  // We can't update non-node types. Pairs should only contain values that are
86
91
  // non-nodes if the yaml document was modified manually after parsing.
87
- if (!yaml.isScalar(pair.key) || typeof pair.key.value !== 'string') {
92
+ if (!yaml.isScalar(pair.key)) {
88
93
  throw new Error('Encountered unexpected non-node value: ' + String(pair.key));
89
94
  }
90
- mapKeyToExistingPair.set(pair.key.value, pair);
91
- }
92
- map.items = Object.entries(target)
93
- .map(([key, value]) => {
95
+ mapKeyToExistingPair.set(ctx.stringifyKey?.(pair.key.value) ?? String(pair.key.value ?? ''), pair);
96
+ }
97
+ const keys = ctx.preserveKeyOrder
98
+ ? [...mapKeyToExistingPair.keys()].filter(key => Object.hasOwn(target, key))
99
+ .concat(Object.keys(target).filter(key => !mapKeyToExistingPair.has(key)))
100
+ : Object.keys(target);
101
+ map.items = keys
102
+ .map(key => {
103
+ const value = target[key];
94
104
  const existingPair = mapKeyToExistingPair.get(key);
95
105
  if (existingPair == null) {
96
106
  return ctx.document.createPair(key, value);
97
107
  }
98
- if (!yaml.isNode(existingPair.value)) {
108
+ if (existingPair.value != null && !yaml.isNode(existingPair.value)) {
99
109
  throw new Error('Encountered unexpected non-node value: ' + String(existingPair.value));
100
110
  }
101
111
  existingPair.value = patchNode(existingPair.value, value, ctx);
@@ -118,10 +128,10 @@ function patchSeq(seq, target, ctx) {
118
128
  // problem becomes important in the future, it may be worth making callers to
119
129
  // pass in a getKeyForNode() function.
120
130
  return isPrimitiveList(target)
121
- ? patchSeqPrimitive(seq, target)
131
+ ? patchSeqPrimitive(seq, target, ctx)
122
132
  : patchSeqComplex(seq, target, ctx);
123
133
  }
124
- function patchSeqPrimitive(seq, target) {
134
+ function patchSeqPrimitive(seq, target, ctx) {
125
135
  // Keep track of existing nodes to reuse when building up the final list from
126
136
  // the target list. These nodes will have comments attached to them, so it's
127
137
  // important to reuse them when possible.
@@ -133,14 +143,14 @@ function patchSeqPrimitive(seq, target) {
133
143
  // We know all items in the target list are scalars. If there's a non-scalar
134
144
  // in the source list, it needs to be removed. Skip over this item so it's
135
145
  // not added to the final list.
136
- if (!yaml.isScalar(item) || !isPrimitive(item.value) || item.value == null) {
146
+ if (!yaml.isScalar(item) || !isPrimitive(item.value) || (item.value == null && ctx.pruneEmptyValues !== false)) {
137
147
  continue;
138
148
  }
139
149
  const nodeList = valueToNodesMap.get(item.value) ?? [];
140
150
  nodeList.push(item);
141
151
  valueToNodesMap.set(item.value, nodeList);
142
152
  }
143
- seq.items = target.filter(item => item != null).map((item) => {
153
+ seq.items = target.filter(item => item != null || ctx.pruneEmptyValues === false).map((item) => {
144
154
  const existingNodesList = valueToNodesMap.get(item);
145
155
  const firstExistingItem = existingNodesList?.shift();
146
156
  // If the list is now empty as a result of removing the first item, clean up
@@ -154,7 +164,7 @@ function patchSeqPrimitive(seq, target) {
154
164
  }
155
165
  function patchSeqComplex(seq, target, ctx) {
156
166
  const nextItems = [];
157
- for (let i = 0; i < Math.max(seq.items.length, target.length); i++) {
167
+ for (let i = 0; i < target.length; i++) {
158
168
  const existingItem = seq.items[i];
159
169
  const targetItem = target[i];
160
170
  if (existingItem != null && !yaml.isNode(existingItem)) {
@@ -0,0 +1,2 @@
1
+ import yaml from 'yaml';
2
+ export declare function preserveScalarAliases(document: yaml.Document): () => void;
@@ -0,0 +1,83 @@
1
+ import yaml from 'yaml';
2
+ export function preserveScalarAliases(document) {
3
+ const groups = new Map();
4
+ const replacements = new Map();
5
+ yaml.visit(document, {
6
+ Scalar(_, node) {
7
+ if (node.anchor)
8
+ groups.set(node, node);
9
+ },
10
+ Alias(_, node) {
11
+ const source = node.resolve(document);
12
+ if (!yaml.isScalar(source))
13
+ return;
14
+ const copy = source.clone();
15
+ copy.anchor = undefined;
16
+ copy.comment = node.comment;
17
+ copy.commentBefore = node.commentBefore;
18
+ copy.spaceBefore = node.spaceBefore;
19
+ groups.set(copy, source);
20
+ replacements.set(node, copy);
21
+ },
22
+ });
23
+ yaml.visit(document, {
24
+ Alias(_, node) {
25
+ return replacements.get(node);
26
+ },
27
+ });
28
+ return () => {
29
+ assignUniqueNames(document, new Set(groups.values()));
30
+ const anchors = new Map();
31
+ yaml.visit(document, {
32
+ Scalar(_, node) {
33
+ const source = groups.get(node);
34
+ if (!source)
35
+ return;
36
+ const anchor = anchors.get(source);
37
+ if (!anchor) {
38
+ node.anchor = source.anchor;
39
+ anchors.set(source, node);
40
+ }
41
+ else if (Object.is(anchor.value, node.value)) {
42
+ const alias = new yaml.Alias(anchor.anchor);
43
+ alias.comment = node.comment;
44
+ alias.commentBefore = node.commentBefore;
45
+ alias.spaceBefore = node.spaceBefore;
46
+ return alias;
47
+ }
48
+ else {
49
+ node.anchor = undefined;
50
+ }
51
+ return undefined;
52
+ },
53
+ });
54
+ };
55
+ }
56
+ function assignUniqueNames(document, sources) {
57
+ const names = new Map();
58
+ yaml.visit(document, {
59
+ Node(_, node) {
60
+ if (yaml.isAlias(node) || !node.anchor)
61
+ return;
62
+ const nodes = names.get(node.anchor) ?? new Set();
63
+ nodes.add(node);
64
+ names.set(node.anchor, nodes);
65
+ },
66
+ });
67
+ const nextSuffix = new Map();
68
+ for (const source of sources) {
69
+ const name = source.anchor;
70
+ const nodes = names.get(name);
71
+ if (!nodes || (nodes.size === 1 && nodes.has(source))) {
72
+ names.set(name, new Set([source]));
73
+ continue;
74
+ }
75
+ let suffix = nextSuffix.get(name) ?? 1;
76
+ while (names.has(`${name}_${suffix}`))
77
+ suffix++;
78
+ nextSuffix.set(name, suffix + 1);
79
+ source.anchor = `${name}_${suffix}`;
80
+ names.set(source.anchor, new Set([source]));
81
+ }
82
+ }
83
+ //# sourceMappingURL=preserveScalarAliases.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/yaml.document-sync",
3
- "version": "1100.0.0",
3
+ "version": "1100.0.2",
4
4
  "description": "Update a YAML document to match the contents of an in-memory object.",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -10,8 +10,11 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "funding": "https://opencollective.com/pnpm",
13
- "repository": "https://github.com/pnpm/pnpm/tree/main/yaml/document-sync",
14
- "homepage": "https://github.com/pnpm/pnpm/tree/main/yaml/document-sync#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/pnpm/pnpm/tree/main/pnpm11/yaml/document-sync"
16
+ },
17
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/yaml/document-sync#readme",
15
18
  "bugs": {
16
19
  "url": "https://github.com/pnpm/pnpm/issues"
17
20
  },
@@ -26,10 +29,11 @@
26
29
  "!*.map"
27
30
  ],
28
31
  "dependencies": {
29
- "yaml": "^2.8.3"
32
+ "yaml": "^2.9.1"
30
33
  },
31
34
  "devDependencies": {
32
- "@pnpm/yaml.document-sync": "1100.0.0"
35
+ "@jest/globals": "30.4.1",
36
+ "@pnpm/yaml.document-sync": "1100.0.2"
33
37
  },
34
38
  "engines": {
35
39
  "node": ">=22.13"