@crewhaus/tool-data 0.7.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.
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Operations over an array of records — the shape almost every API response,
3
+ * CSV file and query result arrives in.
4
+ *
5
+ * Field references are dotted paths (`user.name`), so nested records work
6
+ * without flattening them first. Ordering is always total and stable: where
7
+ * two rows compare equal the original order is kept, so the same input gives
8
+ * the same output every time.
9
+ */
10
+ import { canonicalStringify, deepEqual, isPlainObject } from "./json";
11
+ /** Read a dotted path out of a record. Returns undefined for any miss. */
12
+ export function getPath(record, path) {
13
+ if (path === "")
14
+ return record;
15
+ let cur = record;
16
+ for (const seg of path.split(".")) {
17
+ if (isPlainObject(cur)) {
18
+ cur = cur[seg];
19
+ continue;
20
+ }
21
+ if (Array.isArray(cur) && /^\d+$/.test(seg)) {
22
+ cur = cur[Number(seg)];
23
+ continue;
24
+ }
25
+ return undefined;
26
+ }
27
+ return cur;
28
+ }
29
+ /** Write a dotted path into a record, creating intermediate objects. */
30
+ export function setPath(target, path, value) {
31
+ const segs = path.split(".");
32
+ let cur = target;
33
+ for (let i = 0; i < segs.length - 1; i++) {
34
+ const seg = segs[i];
35
+ const next = cur[seg];
36
+ if (!isPlainObject(next)) {
37
+ const fresh = {};
38
+ cur[seg] = fresh;
39
+ cur = fresh;
40
+ }
41
+ else {
42
+ cur = next;
43
+ }
44
+ }
45
+ cur[segs[segs.length - 1]] = value;
46
+ }
47
+ export class PredicateError extends Error {
48
+ }
49
+ /** Evaluate one condition against a record. */
50
+ export function testCondition(record, cond) {
51
+ const actual = getPath(record, cond.field);
52
+ const expected = cond.value;
53
+ switch (cond.op) {
54
+ case "exists":
55
+ return actual !== undefined && actual !== null;
56
+ case "empty":
57
+ return (actual === undefined ||
58
+ actual === null ||
59
+ actual === "" ||
60
+ (Array.isArray(actual) && actual.length === 0) ||
61
+ (isPlainObject(actual) && Object.keys(actual).length === 0));
62
+ case "eq":
63
+ return deepEqual(actual, expected);
64
+ case "ne":
65
+ return !deepEqual(actual, expected);
66
+ case "in":
67
+ return Array.isArray(expected) && expected.some((e) => deepEqual(actual, e));
68
+ case "contains": {
69
+ if (Array.isArray(actual))
70
+ return actual.some((e) => deepEqual(e, expected));
71
+ return String(actual ?? "").includes(String(expected ?? ""));
72
+ }
73
+ case "startsWith":
74
+ return String(actual ?? "").startsWith(String(expected ?? ""));
75
+ case "endsWith":
76
+ return String(actual ?? "").endsWith(String(expected ?? ""));
77
+ case "matches": {
78
+ if (typeof expected !== "string") {
79
+ throw new PredicateError(`"matches" needs a regular-expression string for ${cond.field}`);
80
+ }
81
+ let re;
82
+ try {
83
+ re = new RegExp(expected);
84
+ }
85
+ catch (err) {
86
+ throw new PredicateError(`invalid regex for ${cond.field}: ${err.message}`);
87
+ }
88
+ return actual !== undefined && actual !== null && re.test(String(actual));
89
+ }
90
+ default:
91
+ return compareOrdered(actual, expected, cond.op);
92
+ }
93
+ }
94
+ function compareOrdered(actual, expected, op) {
95
+ const cmp = compareValues(actual, expected);
96
+ if (cmp === null)
97
+ return false;
98
+ if (op === "lt")
99
+ return cmp < 0;
100
+ if (op === "lte")
101
+ return cmp <= 0;
102
+ if (op === "gt")
103
+ return cmp > 0;
104
+ if (op === "gte")
105
+ return cmp >= 0;
106
+ return false;
107
+ }
108
+ /**
109
+ * Order two scalars, or null when they are not comparable. Numbers compare
110
+ * numerically, strings by code unit, booleans false-before-true; a
111
+ * number/string pair is not comparable, which is what stops `"10" < 9`
112
+ * quietly being true.
113
+ */
114
+ export function compareValues(a, b) {
115
+ if (typeof a === "number" && typeof b === "number")
116
+ return a === b ? 0 : a < b ? -1 : 1;
117
+ if (typeof a === "string" && typeof b === "string")
118
+ return a === b ? 0 : a < b ? -1 : 1;
119
+ if (typeof a === "boolean" && typeof b === "boolean")
120
+ return a === b ? 0 : a ? 1 : -1;
121
+ return null;
122
+ }
123
+ /** Evaluate a whole predicate. An empty predicate matches everything. */
124
+ export function testPredicate(record, predicate) {
125
+ const all = predicate.all ?? [];
126
+ const any = predicate.any ?? [];
127
+ const none = predicate.none ?? [];
128
+ for (const c of all)
129
+ if (!testCondition(record, c))
130
+ return false;
131
+ for (const c of none)
132
+ if (testCondition(record, c))
133
+ return false;
134
+ if (any.length > 0 && !any.some((c) => testCondition(record, c)))
135
+ return false;
136
+ return true;
137
+ }
138
+ /**
139
+ * Sort records by one or more keys. The sort is stable and total: values of
140
+ * different types are ordered by a fixed type rank (null < boolean < number
141
+ * < string < array < object) so the result never depends on the input order
142
+ * of incomparable rows.
143
+ */
144
+ export function sortRecords(records, keys) {
145
+ const decorated = records.map((value, index) => ({ value, index }));
146
+ decorated.sort((left, right) => {
147
+ for (const key of keys) {
148
+ const dir = key.direction === "desc" ? -1 : 1;
149
+ let a = getPath(left.value, key.field);
150
+ let b = getPath(right.value, key.field);
151
+ const aNull = a === undefined || a === null;
152
+ const bNull = b === undefined || b === null;
153
+ if (aNull || bNull) {
154
+ if (aNull && bNull)
155
+ continue;
156
+ const nullsLast = (key.nulls ?? "last") === "last";
157
+ return aNull === nullsLast ? 1 : -1;
158
+ }
159
+ if (key.caseInsensitive === true) {
160
+ if (typeof a === "string")
161
+ a = a.toLowerCase();
162
+ if (typeof b === "string")
163
+ b = b.toLowerCase();
164
+ }
165
+ const cmp = totalCompare(a, b);
166
+ if (cmp !== 0)
167
+ return cmp * dir;
168
+ }
169
+ return left.index - right.index;
170
+ });
171
+ return decorated.map((d) => d.value);
172
+ }
173
+ /**
174
+ * The type-rank order. `TYPE_ORDER[rankOf(v)]` is the name of `v`'s rank, so
175
+ * the documented order and the code that implements it cannot drift.
176
+ * `undefined` ranks with `null`, since neither is a JSON value a comparison
177
+ * can say anything else about.
178
+ */
179
+ export const TYPE_ORDER = Object.freeze([
180
+ "null",
181
+ "boolean",
182
+ "number",
183
+ "string",
184
+ "array",
185
+ "object",
186
+ ]);
187
+ function rankOf(v) {
188
+ if (v === null || v === undefined)
189
+ return 0;
190
+ if (typeof v === "boolean")
191
+ return 1;
192
+ if (typeof v === "number")
193
+ return 2;
194
+ if (typeof v === "string")
195
+ return 3;
196
+ if (Array.isArray(v))
197
+ return 4;
198
+ return 5;
199
+ }
200
+ /**
201
+ * A total order over JSON values, used so a sort is never arbitrary.
202
+ *
203
+ * Within a rank, scalars compare naturally. Arrays and objects compare by
204
+ * their canonical text, which is lexicographic and therefore *not*
205
+ * element-wise numeric: `[1, 10]` sorts before `[1, 2]`, because `"1"`
206
+ * precedes `"2"` at the fourth character. That is the price of an order that
207
+ * is total and never has to invent a comparison between a number and an
208
+ * object; sort by a scalar field when the order has to mean something.
209
+ */
210
+ export function totalCompare(a, b) {
211
+ const ra = rankOf(a);
212
+ const rb = rankOf(b);
213
+ if (ra !== rb)
214
+ return ra < rb ? -1 : 1;
215
+ const direct = compareValues(a, b);
216
+ if (direct !== null)
217
+ return direct;
218
+ // null/undefined against each other, and arrays and objects: order by
219
+ // canonical text, which is exact and stable. A hash would be neither —
220
+ // two different values that collided would compare equal.
221
+ const sa = canonicalStringify(a);
222
+ const sb = canonicalStringify(b);
223
+ return sa === sb ? 0 : sa < sb ? -1 : 1;
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Projection, grouping, joining
227
+ /** Keep only the named fields (dotted paths), preserving the requested order. */
228
+ export function selectFields(record, fields) {
229
+ const out = {};
230
+ for (const field of fields) {
231
+ const value = getPath(record, field);
232
+ if (value !== undefined)
233
+ setPath(out, field, value);
234
+ }
235
+ return out;
236
+ }
237
+ /** Drop the named fields (dotted paths) from a shallow copy. */
238
+ export function omitFields(record, fields) {
239
+ const out = structuredCloneish(record);
240
+ for (const field of fields) {
241
+ const segs = field.split(".");
242
+ let cur = out;
243
+ for (let i = 0; i < segs.length - 1; i++) {
244
+ if (!isPlainObject(cur))
245
+ break;
246
+ cur = cur[segs[i]];
247
+ }
248
+ if (isPlainObject(cur))
249
+ delete cur[segs[segs.length - 1]];
250
+ }
251
+ return out;
252
+ }
253
+ function structuredCloneish(value) {
254
+ const out = {};
255
+ for (const k of Object.keys(value)) {
256
+ const v = value[k];
257
+ out[k] = isPlainObject(v) ? structuredCloneish(v) : v;
258
+ }
259
+ return out;
260
+ }
261
+ /**
262
+ * Group records by one or more fields and reduce each group.
263
+ *
264
+ * `sum` and `avg` ignore non-numeric values and report how many were skipped
265
+ * via the `_skipped` count, rather than producing NaN. `min`/`max` use the
266
+ * same total order as `sortRecords`. `distinct` counts distinct values by
267
+ * their canonical form, so `{a:1,b:2}` and `{b:2,a:1}` are one value.
268
+ */
269
+ export function aggregate(records, groupBy, aggregations) {
270
+ const buckets = new Map();
271
+ for (const rec of records) {
272
+ const key = {};
273
+ for (const field of groupBy)
274
+ key[field] = getPath(rec, field) ?? null;
275
+ const id = canonicalStringify(key);
276
+ const existing = buckets.get(id);
277
+ if (existing === undefined)
278
+ buckets.set(id, { key, rows: [rec] });
279
+ else
280
+ existing.rows.push(rec);
281
+ }
282
+ let skipped = 0;
283
+ const groups = [];
284
+ for (const bucket of buckets.values()) {
285
+ const out = { ...bucket.key, count: bucket.rows.length };
286
+ for (const agg of aggregations) {
287
+ const field = agg.field ?? "";
288
+ const values = bucket.rows.map((r) => getPath(r, field));
289
+ switch (agg.fn) {
290
+ case "count":
291
+ out[agg.as] = field === "" ? bucket.rows.length : values.filter(isPresent).length;
292
+ break;
293
+ case "sum":
294
+ case "avg": {
295
+ const nums = values.filter((v) => typeof v === "number");
296
+ skipped += values.filter(isPresent).length - nums.length;
297
+ const total = nums.reduce((a, b) => a + b, 0);
298
+ out[agg.as] = agg.fn === "sum" ? total : nums.length === 0 ? null : total / nums.length;
299
+ break;
300
+ }
301
+ case "min":
302
+ case "max": {
303
+ const present = values.filter(isPresent);
304
+ if (present.length === 0) {
305
+ out[agg.as] = null;
306
+ break;
307
+ }
308
+ out[agg.as] = present.reduce((best, v) => {
309
+ const cmp = totalCompare(v, best);
310
+ return (agg.fn === "min" ? cmp < 0 : cmp > 0) ? v : best;
311
+ });
312
+ break;
313
+ }
314
+ case "first":
315
+ out[agg.as] = values[0] ?? null;
316
+ break;
317
+ case "last":
318
+ out[agg.as] = values[values.length - 1] ?? null;
319
+ break;
320
+ case "distinct":
321
+ out[agg.as] = new Set(values.filter(isPresent).map((v) => canonicalStringify(v))).size;
322
+ break;
323
+ default:
324
+ break;
325
+ }
326
+ }
327
+ groups.push(out);
328
+ }
329
+ return { groups, skipped };
330
+ }
331
+ function isPresent(v) {
332
+ return v !== undefined && v !== null;
333
+ }
334
+ /**
335
+ * Join two record arrays on a key. Matching is by the canonical form of the
336
+ * key value, so `1` and `"1"` are different keys — a silent coercion here is
337
+ * how a join quietly loses rows.
338
+ *
339
+ * Every right-hand match is emitted, so a one-to-many join multiplies rows,
340
+ * as SQL does. `maxRows` bounds that.
341
+ */
342
+ export function joinRecords(left, right, options) {
343
+ const index = new Map();
344
+ for (const r of right) {
345
+ const key = getPath(r, options.rightKey);
346
+ if (!isPresent(key))
347
+ continue;
348
+ const id = canonicalStringify(key);
349
+ const bucket = index.get(id);
350
+ if (bucket === undefined)
351
+ index.set(id, [r]);
352
+ else
353
+ bucket.push(r);
354
+ }
355
+ const rows = [];
356
+ const matchedRight = new Set();
357
+ let unmatchedLeft = 0;
358
+ let truncated = false;
359
+ const push = (row) => {
360
+ if (rows.length >= options.maxRows) {
361
+ truncated = true;
362
+ return;
363
+ }
364
+ rows.push(row);
365
+ };
366
+ for (const l of left) {
367
+ const key = getPath(l, options.leftKey);
368
+ const id = isPresent(key) ? canonicalStringify(key) : null;
369
+ const matches = id === null ? undefined : index.get(id);
370
+ if (matches === undefined || matches.length === 0) {
371
+ unmatchedLeft += 1;
372
+ if (options.kind === "left" || options.kind === "full")
373
+ push({ ...l });
374
+ continue;
375
+ }
376
+ if (id !== null)
377
+ matchedRight.add(id);
378
+ for (const r of matches)
379
+ push(mergeRow(l, r, options.rightPrefix));
380
+ }
381
+ let unmatchedRight = 0;
382
+ for (const r of right) {
383
+ const key = getPath(r, options.rightKey);
384
+ const id = isPresent(key) ? canonicalStringify(key) : null;
385
+ if (id === null || !matchedRight.has(id)) {
386
+ unmatchedRight += 1;
387
+ if (options.kind === "right" || options.kind === "full")
388
+ push({ ...r });
389
+ }
390
+ }
391
+ return { rows, truncated, unmatchedLeft, unmatchedRight };
392
+ }
393
+ function mergeRow(left, right, prefix) {
394
+ const out = { ...left };
395
+ for (const k of Object.keys(right)) {
396
+ out[Object.hasOwn(left, k) ? `${prefix}${k}` : k] = right[k];
397
+ }
398
+ return out;
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // Reshaping
402
+ /** Records to a column-oriented object: `[{a:1},{a:2}]` becomes `{a:[1,2]}`. */
403
+ export function recordsToColumns(records) {
404
+ const keys = [];
405
+ const seen = new Set();
406
+ for (const rec of records) {
407
+ for (const k of Object.keys(rec)) {
408
+ if (!seen.has(k)) {
409
+ seen.add(k);
410
+ keys.push(k);
411
+ }
412
+ }
413
+ }
414
+ const out = {};
415
+ for (const k of keys)
416
+ out[k] = records.map((r) => (Object.hasOwn(r, k) ? r[k] : null));
417
+ return out;
418
+ }
419
+ /**
420
+ * Columns back to records. Columns of differing length are padded with null
421
+ * up to the longest, and the caller is told how many were short.
422
+ */
423
+ export function columnsToRecords(columns) {
424
+ const keys = Object.keys(columns);
425
+ const lengths = keys.map((k) => (columns[k] ?? []).length);
426
+ const max = lengths.length === 0 ? 0 : Math.max(...lengths);
427
+ const ragged = keys.filter((k) => (columns[k] ?? []).length !== max);
428
+ const records = [];
429
+ for (let i = 0; i < max; i++) {
430
+ const rec = {};
431
+ for (const k of keys) {
432
+ const col = columns[k] ?? [];
433
+ rec[k] = i < col.length ? col[i] : null;
434
+ }
435
+ records.push(rec);
436
+ }
437
+ return { records, ragged };
438
+ }
439
+ /** Flatten nested objects into dotted keys. Arrays become `a.0`, `a.1` when expanded. */
440
+ export function flattenObject(value, separator, expandArrays, maxDepth) {
441
+ const out = {};
442
+ const walk = (node, prefix, depth) => {
443
+ if (depth >= maxDepth) {
444
+ out[prefix] = node;
445
+ return;
446
+ }
447
+ if (isPlainObject(node)) {
448
+ const keys = Object.keys(node);
449
+ if (keys.length === 0) {
450
+ if (prefix !== "")
451
+ out[prefix] = {};
452
+ return;
453
+ }
454
+ for (const k of keys)
455
+ walk(node[k], prefix === "" ? k : `${prefix}${separator}${k}`, depth + 1);
456
+ return;
457
+ }
458
+ if (Array.isArray(node) && expandArrays) {
459
+ if (node.length === 0) {
460
+ if (prefix !== "")
461
+ out[prefix] = [];
462
+ return;
463
+ }
464
+ node.forEach((el, i) => walk(el, prefix === "" ? String(i) : `${prefix}${separator}${i}`, depth + 1));
465
+ return;
466
+ }
467
+ out[prefix] = node;
468
+ };
469
+ walk(value, "", 0);
470
+ return out;
471
+ }
472
+ /**
473
+ * Rebuild nested objects from dotted keys. A numeric segment becomes an
474
+ * array index when `arraysFromNumericKeys` is on and the keys under that
475
+ * prefix are a contiguous run from 0; otherwise it stays an object key, so
476
+ * `{"a.2": 1}` does not silently produce two nulls.
477
+ */
478
+ export function unflattenObject(flat, separator, arraysFromNumericKeys) {
479
+ const root = {};
480
+ for (const key of Object.keys(flat)) {
481
+ const segs = key.split(separator);
482
+ let cur = root;
483
+ for (let i = 0; i < segs.length - 1; i++) {
484
+ const seg = segs[i];
485
+ const next = cur[seg];
486
+ if (!isPlainObject(next)) {
487
+ const fresh = {};
488
+ cur[seg] = fresh;
489
+ cur = fresh;
490
+ }
491
+ else {
492
+ cur = next;
493
+ }
494
+ }
495
+ cur[segs[segs.length - 1]] = flat[key];
496
+ }
497
+ return arraysFromNumericKeys ? arrayify(root) : root;
498
+ }
499
+ function arrayify(node) {
500
+ if (!isPlainObject(node))
501
+ return node;
502
+ const keys = Object.keys(node);
503
+ const converted = {};
504
+ for (const k of keys)
505
+ converted[k] = arrayify(node[k]);
506
+ if (keys.length === 0)
507
+ return converted;
508
+ const allNumeric = keys.every((k) => /^(0|[1-9][0-9]*)$/.test(k));
509
+ if (!allNumeric)
510
+ return converted;
511
+ const indices = keys.map(Number).sort((a, b) => a - b);
512
+ const contiguous = indices.every((n, i) => n === i);
513
+ if (!contiguous)
514
+ return converted;
515
+ return indices.map((n) => converted[String(n)]);
516
+ }
517
+ /**
518
+ * De-duplicate records by a key path, or by the whole value when no key is
519
+ * given. Identity is the canonical (key-sorted) serialization, so two
520
+ * records that differ only in key order are one record.
521
+ */
522
+ export function dedupeRecords(records, keyFields, keep) {
523
+ const seen = new Map();
524
+ const out = [];
525
+ for (const rec of records) {
526
+ const id = keyFields.length === 0
527
+ ? canonicalStringify(rec)
528
+ : canonicalStringify(keyFields.map((f) => getPath(rec, f) ?? null));
529
+ const at = seen.get(id);
530
+ if (at === undefined) {
531
+ seen.set(id, out.length);
532
+ out.push(rec);
533
+ continue;
534
+ }
535
+ if (keep === "last")
536
+ out[at] = rec;
537
+ }
538
+ return { records: out, removed: records.length - out.length };
539
+ }
540
+ /**
541
+ * Take a deterministic sample. There is no random mode on purpose: a tool
542
+ * that returned a different subset on each call could not be cached, retried
543
+ * or compared between runs. `evenly` spreads picks across the whole array.
544
+ */
545
+ export function sampleRecords(records, mode, count, step) {
546
+ if (records.length === 0)
547
+ return [];
548
+ switch (mode) {
549
+ case "head":
550
+ return records.slice(0, count);
551
+ case "tail":
552
+ return records.slice(Math.max(0, records.length - count));
553
+ case "everyNth": {
554
+ const out = [];
555
+ for (let i = 0; i < records.length && out.length < count; i += Math.max(1, step)) {
556
+ out.push(records[i]);
557
+ }
558
+ return out;
559
+ }
560
+ default: {
561
+ const n = Math.min(count, records.length);
562
+ if (n <= 1)
563
+ return n === 1 ? [records[0]] : [];
564
+ const out = [];
565
+ for (let i = 0; i < n; i++) {
566
+ out.push(records[Math.round((i * (records.length - 1)) / (n - 1))]);
567
+ }
568
+ return out;
569
+ }
570
+ }
571
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A hand-written TOML reader and writer. As with the YAML module, there is
3
+ * no dependency behind it, so the supported subset below is the contract.
4
+ *
5
+ * ## Read: supported
6
+ *
7
+ * - `key = value` pairs, bare, quoted and dotted keys (`a.b.c = 1`).
8
+ * - Tables `[a.b]` and arrays of tables `[[a.b]]`, at any depth.
9
+ * - Basic strings `"..."` with the standard escapes including `\uXXXX` and
10
+ * `\UXXXXXXXX`, literal strings `'...'`, and the multi-line `"""..."""`
11
+ * and `'''...'''` forms, with the leading-newline trim and the backslash
12
+ * line continuation.
13
+ * - Integers, including `_` separators and `0x` / `0o` / `0b` literals.
14
+ * - Floats, including exponents, `inf` and `nan`.
15
+ * - Booleans, arrays (nested, multi-line, with trailing commas) and inline
16
+ * tables `{ a = 1, b = "x" }`.
17
+ * - `#` comments.
18
+ *
19
+ * ## Read: deliberate departures, each documented rather than silent
20
+ *
21
+ * - **Dates and times stay strings.** TOML has first-class offset date-time,
22
+ * local date-time, date and time types. JSON has none of them, so
23
+ * converting to a `Date` would make a round trip lossy. `2026-01-01` reads
24
+ * back as the string `"2026-01-01"`.
25
+ * - Redefining a table or key is an error, as the spec requires — including
26
+ * both halves of the rule: a `[header]` may not reopen a table a dotted
27
+ * key created, and a dotted key may not reach into a table a `[header]`
28
+ * already defined.
29
+ * - `inf` and `nan` read as the JavaScript infinities and NaN and write back
30
+ * out as `inf` / `nan`, but JSON cannot spell them, so converting to JSON
31
+ * renders them `null`.
32
+ * - Integers outside the JavaScript safe range are kept as strings rather
33
+ * than silently rounded.
34
+ *
35
+ * ## Write
36
+ *
37
+ * Scalars and scalar arrays first, then sub-tables, then arrays of tables —
38
+ * which is the order that reads back identically. A value TOML cannot hold
39
+ * (`null`) is skipped, and the writer reports which keys it dropped rather
40
+ * than pretending the output is complete.
41
+ */
42
+ export declare class TomlError extends Error {
43
+ readonly line: number;
44
+ constructor(message: string, line: number);
45
+ }
46
+ /** Parse a TOML document into JSON values. Throws `TomlError` with a line number. */
47
+ export declare function parseToml(text: string): Record<string, unknown>;
48
+ /** Type a bare TOML token. Exported so the tests can pin the exact rules. */
49
+ export declare function scalarFromToken(raw: string, line: number): unknown;
50
+ export type TomlWriteResult = {
51
+ text: string;
52
+ skipped: string[];
53
+ };
54
+ /**
55
+ * Serialize an object as TOML. Returns the text plus the dotted paths of any
56
+ * keys that were dropped because TOML has no representation for them
57
+ * (`null` and `undefined`).
58
+ */
59
+ export declare function stringifyToml(value: unknown): TomlWriteResult;