@zhin.js/config-yaml 1.0.2 → 1.0.4

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.
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, resolve } from 'node:path';
4
4
  import { ConfigPatchPathError, } from '@zhin.js/runtime';
5
- import { parseDocument } from 'yaml';
5
+ import { isMap, isSeq, parseDocument } from 'yaml';
6
6
  export class YamlConfigDocumentError extends Error {
7
7
  constructor(message, options) {
8
8
  super(message, options);
@@ -125,9 +125,51 @@ function applyPatch(document, patch) {
125
125
  return;
126
126
  }
127
127
  if (patch.op === 'set')
128
- document.setIn(patch.path, structuredClone(patch.value));
128
+ document.setIn(resolvePath(document, patch.path), structuredClone(patch.value));
129
129
  else
130
- document.deleteIn(patch.path);
130
+ document.deleteIn(resolvePath(document, patch.path));
131
+ }
132
+ /**
133
+ * Walks the AST so numeric segments address sequence elements (`endpoints.0.url`).
134
+ * Non-numeric segments on a sequence and out-of-bounds indexes are rejected
135
+ * with the same errors as the Runtime planner; missing intermediates are left
136
+ * for `setIn` to create as mappings (matching planner semantics).
137
+ */
138
+ function resolvePath(document, path) {
139
+ const resolved = [];
140
+ let node = document.contents;
141
+ for (const [index, segment] of path.entries()) {
142
+ const isLast = index === path.length - 1;
143
+ if (isSeq(node)) {
144
+ if (!/^(0|[1-9]\d*)$/.test(segment)) {
145
+ throw new ConfigPatchPathError(`Config path ${pointer(path.slice(0, index + 1))} requires an array index, got "${segment}"`);
146
+ }
147
+ const arrayIndex = Number(segment);
148
+ if (arrayIndex >= node.items.length) {
149
+ throw new ConfigPatchPathError(`Config path ${pointer(path.slice(0, index + 1))} is out of bounds (array length ${node.items.length})`);
150
+ }
151
+ resolved.push(arrayIndex);
152
+ node = isLast ? undefined : node.get(arrayIndex, false);
153
+ }
154
+ else if (isMap(node)) {
155
+ resolved.push(segment);
156
+ node = isLast ? undefined : node.get(segment, false);
157
+ }
158
+ else if (node == null) {
159
+ // Missing intermediate mapping (setIn creates it).
160
+ resolved.push(segment);
161
+ node = undefined;
162
+ }
163
+ else {
164
+ throw new ConfigPatchPathError(`Config path ${pointer(path.slice(0, index + 1))} is not an object`);
165
+ }
166
+ }
167
+ return resolved;
168
+ }
169
+ function pointer(path) {
170
+ if (path.length === 0)
171
+ return '/';
172
+ return `/${path.map((segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1')).join('/')}`;
131
173
  }
132
174
  function assertPath(path) {
133
175
  for (const segment of path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/config-yaml",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Comment-preserving YAML ConfigDocument adapter for the Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -17,12 +17,12 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "yaml": "2.9.0",
20
- "@zhin.js/runtime": "1.0.2"
20
+ "@zhin.js/runtime": "1.0.4"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^26.1.0",
24
24
  "typescript": "^6.0.3",
25
- "@zhin.js/plugin-runtime": "1.1.0"
25
+ "@zhin.js/plugin-runtime": "1.1.1"
26
26
  },
27
27
  "engines": {
28
28
  "node": "^20.19.0 || >=22.12.0"