@dxos/util 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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.
Files changed (142) hide show
  1. package/dist/lib/browser/index.mjs +569 -203
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +569 -203
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/array-to-hex.d.ts.map +1 -1
  8. package/dist/types/src/array.d.ts +3 -2
  9. package/dist/types/src/array.d.ts.map +1 -1
  10. package/dist/types/src/assume.d.ts.map +1 -1
  11. package/dist/types/src/binder.d.ts.map +1 -1
  12. package/dist/types/src/bitfield.d.ts.map +1 -1
  13. package/dist/types/src/browser-storage.d.ts +9 -0
  14. package/dist/types/src/browser-storage.d.ts.map +1 -0
  15. package/dist/types/src/callback-collection.d.ts.map +1 -1
  16. package/dist/types/src/callback.d.ts.map +1 -1
  17. package/dist/types/src/case.d.ts.map +1 -1
  18. package/dist/types/src/chunk-array.d.ts.map +1 -1
  19. package/dist/types/src/circular-buffer.d.ts +1 -0
  20. package/dist/types/src/circular-buffer.d.ts.map +1 -1
  21. package/dist/types/src/clear-undefined.d.ts.map +1 -1
  22. package/dist/types/src/complex.d.ts +15 -0
  23. package/dist/types/src/complex.d.ts.map +1 -1
  24. package/dist/types/src/composite-key.d.ts +10 -0
  25. package/dist/types/src/composite-key.d.ts.map +1 -0
  26. package/dist/types/src/composite-key.test.d.ts +2 -0
  27. package/dist/types/src/composite-key.test.d.ts.map +1 -0
  28. package/dist/types/src/deep.d.ts +0 -3
  29. package/dist/types/src/deep.d.ts.map +1 -1
  30. package/dist/types/src/defer-function.d.ts.map +1 -1
  31. package/dist/types/src/defer.d.ts +1 -1
  32. package/dist/types/src/defer.d.ts.map +1 -1
  33. package/dist/types/src/di-container.d.ts.map +1 -1
  34. package/dist/types/src/di-key.d.ts +7 -5
  35. package/dist/types/src/di-key.d.ts.map +1 -1
  36. package/dist/types/src/entry.d.ts.map +1 -1
  37. package/dist/types/src/error-format.d.ts +5 -0
  38. package/dist/types/src/error-format.d.ts.map +1 -0
  39. package/dist/types/src/filename.d.ts +9 -0
  40. package/dist/types/src/filename.d.ts.map +1 -0
  41. package/dist/types/src/for-each-async.d.ts.map +1 -1
  42. package/dist/types/src/human-hash.d.ts.map +1 -1
  43. package/dist/types/src/index.d.ts +7 -2
  44. package/dist/types/src/index.d.ts.map +1 -1
  45. package/dist/types/src/instance-id.d.ts.map +1 -1
  46. package/dist/types/src/interval.d.ts.map +1 -1
  47. package/dist/types/src/join-tables.d.ts.map +1 -1
  48. package/dist/types/src/json.d.ts +1 -1
  49. package/dist/types/src/json.d.ts.map +1 -1
  50. package/dist/types/src/map-values.d.ts.map +1 -1
  51. package/dist/types/src/map.d.ts.map +1 -1
  52. package/dist/types/src/object.d.ts +3 -0
  53. package/dist/types/src/object.d.ts.map +1 -1
  54. package/dist/types/src/order-keys.d.ts.map +1 -1
  55. package/dist/types/src/order.d.ts.map +1 -1
  56. package/dist/types/src/pick.d.ts.map +1 -1
  57. package/dist/types/src/platform.d.ts +5 -2
  58. package/dist/types/src/platform.d.ts.map +1 -1
  59. package/dist/types/src/position.d.ts +1 -1
  60. package/dist/types/src/position.d.ts.map +1 -1
  61. package/dist/types/src/random.d.ts.map +1 -1
  62. package/dist/types/src/range.d.ts.map +1 -1
  63. package/dist/types/src/reducers.d.ts.map +1 -1
  64. package/dist/types/src/remove-undefined-keys.d.ts.map +1 -1
  65. package/dist/types/src/retry.d.ts +32 -0
  66. package/dist/types/src/retry.d.ts.map +1 -0
  67. package/dist/types/src/safe-await.d.ts.map +1 -1
  68. package/dist/types/src/safe-instanceof.d.ts.map +1 -1
  69. package/dist/types/src/safe-parse.d.ts +7 -4
  70. package/dist/types/src/safe-parse.d.ts.map +1 -1
  71. package/dist/types/src/safe-stringify.d.ts +22 -0
  72. package/dist/types/src/safe-stringify.d.ts.map +1 -0
  73. package/dist/types/src/safe-stringify.test.d.ts +2 -0
  74. package/dist/types/src/safe-stringify.test.d.ts.map +1 -0
  75. package/dist/types/src/sliding-window-summary.d.ts.map +1 -1
  76. package/dist/types/src/sort.d.ts.map +1 -1
  77. package/dist/types/src/string.d.ts +8 -0
  78. package/dist/types/src/string.d.ts.map +1 -1
  79. package/dist/types/src/string.test.d.ts +2 -0
  80. package/dist/types/src/string.test.d.ts.map +1 -0
  81. package/dist/types/src/sum.d.ts.map +1 -1
  82. package/dist/types/src/throw-unhandled-error.d.ts.map +1 -1
  83. package/dist/types/src/to-fallback.d.ts +18 -4
  84. package/dist/types/src/to-fallback.d.ts.map +1 -1
  85. package/dist/types/src/tracer.d.ts.map +1 -1
  86. package/dist/types/src/tree.d.ts.map +1 -1
  87. package/dist/types/src/types.d.ts +24 -6
  88. package/dist/types/src/types.d.ts.map +1 -1
  89. package/dist/types/src/uint8array.d.ts.map +1 -1
  90. package/dist/types/src/unit.d.ts +14 -0
  91. package/dist/types/src/unit.d.ts.map +1 -0
  92. package/dist/types/src/unit.test.d.ts +2 -0
  93. package/dist/types/src/unit.test.d.ts.map +1 -0
  94. package/dist/types/src/url.d.ts.map +1 -1
  95. package/dist/types/src/weak.d.ts +15 -0
  96. package/dist/types/src/weak.d.ts.map +1 -1
  97. package/dist/types/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +14 -16
  99. package/src/array.test.ts +1 -1
  100. package/src/array.ts +9 -4
  101. package/src/assume.ts +0 -1
  102. package/src/binder.ts +2 -2
  103. package/src/browser-storage.ts +60 -0
  104. package/src/circular-buffer.test.ts +27 -1
  105. package/src/circular-buffer.ts +5 -0
  106. package/src/complex.test.ts +1 -1
  107. package/src/complex.ts +32 -0
  108. package/src/composite-key.test.ts +31 -0
  109. package/src/composite-key.ts +16 -0
  110. package/src/deep.ts +2 -6
  111. package/src/defer.ts +1 -1
  112. package/src/di-key.ts +10 -8
  113. package/src/error-format.ts +24 -0
  114. package/src/filename.ts +16 -0
  115. package/src/human-hash.test.ts +1 -1
  116. package/src/index.ts +7 -2
  117. package/src/json.ts +2 -7
  118. package/src/object.ts +3 -0
  119. package/src/platform.ts +35 -3
  120. package/src/position.test.ts +2 -2
  121. package/src/retry.ts +74 -0
  122. package/src/safe-parse.ts +29 -15
  123. package/src/safe-stringify.test.ts +96 -0
  124. package/src/safe-stringify.ts +153 -0
  125. package/src/sort.test.ts +1 -1
  126. package/src/string.test.ts +19 -0
  127. package/src/string.ts +40 -0
  128. package/src/to-fallback.ts +44 -160
  129. package/src/tree.test.ts +1 -1
  130. package/src/types.test.ts +11 -1
  131. package/src/types.ts +40 -12
  132. package/src/uint8array.test.ts +1 -1
  133. package/src/unit.test.ts +25 -0
  134. package/src/unit.ts +87 -0
  135. package/src/weak.test.ts +1 -1
  136. package/src/weak.ts +52 -14
  137. package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
  138. package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
  139. package/dist/types/src/first-two-chars.d.ts +0 -9
  140. package/dist/types/src/first-two-chars.d.ts.map +0 -1
  141. package/src/explicit-resource-management-polyfill.ts +0 -13
  142. package/src/first-two-chars.ts +0 -44
package/src/retry.ts ADDED
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export interface RetryOptions<T> {
6
+ /**
7
+ * @default 3
8
+ */
9
+ count?: number;
10
+
11
+ /**
12
+ * @default 100
13
+ */
14
+ delayMs?: number;
15
+
16
+ /**
17
+ * Factor to increase delay by.
18
+ * @default 2
19
+ */
20
+ exponent?: number;
21
+
22
+ /**
23
+ * Retry if error matches the predicate.
24
+ * @default () => true
25
+ */
26
+ retryOnError?: (error: unknown) => Promise<boolean>;
27
+
28
+ /**
29
+ * Retry if value matches the predicate.
30
+ * @default () => false
31
+ */
32
+ retryOnValue?: (value: T) => Promise<boolean>;
33
+ }
34
+
35
+ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions<unknown>> = {
36
+ count: 3,
37
+ delayMs: 100,
38
+ exponent: 2,
39
+ retryOnError: async () => true,
40
+ retryOnValue: async () => false,
41
+ };
42
+
43
+ /**
44
+ * Retries the operation a number of times.
45
+ * @returns The result of the succesfull invocation
46
+ * @throws Last error if all retries failed
47
+ */
48
+ export const retry = async <T>(options: RetryOptions<T>, cb: () => Promise<T>): Promise<T> => {
49
+ const fullOptions: Required<RetryOptions<T>> = { ...DEFAULT_RETRY_OPTIONS, ...options };
50
+
51
+ let numRetries = 0,
52
+ currentDelay = fullOptions.delayMs;
53
+ while (true) {
54
+ let result: T;
55
+ try {
56
+ result = await cb();
57
+ } catch (err) {
58
+ if (numRetries > fullOptions.count || !(await fullOptions.retryOnError(err))) {
59
+ throw err;
60
+ }
61
+ await new Promise((resolve) => setTimeout(resolve, currentDelay));
62
+ currentDelay *= fullOptions.exponent;
63
+ numRetries++;
64
+ continue;
65
+ }
66
+ if (!(await fullOptions.retryOnValue(result))) {
67
+ return result;
68
+ }
69
+ await new Promise((resolve) => setTimeout(resolve, currentDelay));
70
+ currentDelay *= fullOptions.exponent;
71
+ numRetries++;
72
+ continue;
73
+ }
74
+ };
package/src/safe-parse.ts CHANGED
@@ -2,31 +2,45 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export const safeParseInt = (value: string | undefined, defaultValue?: number) => {
5
+ export function safeParseInt(str: string | undefined, defaultValue: number): number;
6
+ export function safeParseInt(str: string | undefined): number | undefined;
7
+ export function safeParseInt(str: string | undefined, defaultValue?: number): number | undefined {
6
8
  try {
7
- const n = parseInt(value ?? '');
8
- return isNaN(n) ? defaultValue : n;
9
- } catch (err) {
9
+ const value = parseInt(str ?? '');
10
+ return isNaN(value) ? defaultValue : value;
11
+ } catch {
10
12
  return defaultValue;
11
13
  }
12
- };
14
+ }
13
15
 
14
- export const safeParseFloat = (str: string, defaultValue?: number): number | undefined => {
16
+ export function safeParseFloat(str: string | undefined, defaultValue: number): number;
17
+ export function safeParseFloat(str: string | undefined): number | undefined;
18
+ export function safeParseFloat(str: string | undefined, defaultValue?: number): number | undefined {
15
19
  try {
16
- return parseFloat(str);
20
+ const value = parseFloat(str ?? '');
21
+ return isNaN(value) ? defaultValue : value;
17
22
  } catch {
18
- return defaultValue ?? 0;
23
+ return defaultValue;
19
24
  }
20
- };
25
+ }
21
26
 
22
27
  export const safeParseJson: {
23
- <T extends object>(data: string | undefined | null, defaultValue: T): T;
24
- <T extends object>(data: string | undefined | null): T | undefined;
25
- } = <T extends object>(data: string | undefined | null, defaultValue?: T) => {
26
- if (data && data.length > 0) {
28
+ <T extends object>(str: string | undefined | null, defaultValue: T): T;
29
+ <T extends object>(str: string | undefined | null): T | undefined;
30
+ } = <T extends object = any>(str: string | undefined | null, defaultValue?: T): T | undefined => {
31
+ if (str && str.length > 0) {
27
32
  try {
28
- return JSON.parse(data);
29
- } catch (err) {}
33
+ return JSON.parse(str);
34
+ } catch {}
30
35
  }
36
+
31
37
  return defaultValue;
32
38
  };
39
+
40
+ export const safeUrl = (str: string | undefined | null): URL | undefined => {
41
+ try {
42
+ return new URL(str ?? '');
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ };
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { SKIP, createReplacer, safeStringify } from './safe-stringify';
8
+
9
+ describe('safeStringify', () => {
10
+ test('stringifies plain nested objects', ({ expect }) => {
11
+ const data = { a: 1, b: { c: 'hello' } };
12
+ const result = safeStringify(data);
13
+ expect(result).to.include('"a": 1');
14
+ expect(result).to.include('"b"');
15
+ expect(result).to.include('"c": "hello"');
16
+ });
17
+
18
+ test('handles circular references (Cycle story)', ({ expect }) => {
19
+ const cycle: Record<string, unknown> = { a: 1, b: [] };
20
+ (cycle.b as unknown[]).push(cycle);
21
+
22
+ const result = safeStringify(cycle);
23
+ expect(result).to.include('"a": 1');
24
+ expect(result).to.include('=> $]');
25
+ expect(result).to.match(/\[\$\.b\.\d+ => \$\]/);
26
+ });
27
+
28
+ test('handles multiple circular references', ({ expect }) => {
29
+ const obj: Record<string, unknown> = { x: 1 };
30
+ obj.self = obj;
31
+
32
+ const result = safeStringify(obj);
33
+ expect(result).to.include('"x": 1');
34
+ expect(result).to.include('[$.self => $]');
35
+ });
36
+
37
+ test('filters functions', ({ expect }) => {
38
+ const data = { a: 1, fn: () => {} };
39
+ const result = safeStringify(data);
40
+ expect(result).to.include('"a": 1');
41
+ expect(result).not.to.include('fn');
42
+ });
43
+
44
+ test('filters exotic objects', ({ expect }) => {
45
+ class Custom {}
46
+ const data = { a: 1, exotic: new Custom() };
47
+ const result = safeStringify(data);
48
+ expect(result).to.include('"a": 1');
49
+ expect(result).not.to.include('exotic');
50
+ });
51
+
52
+ test('preserves arrays', ({ expect }) => {
53
+ const data = { arr: [1, 2, 3] };
54
+ const result = safeStringify(data);
55
+ expect(result).to.include('"arr"');
56
+ expect(result).to.include('1');
57
+ expect(result).to.include('2');
58
+ expect(result).to.include('3');
59
+ });
60
+ });
61
+
62
+ describe('createReplacer', () => {
63
+ test('maxArrayLen truncates long arrays (Large story)', ({ expect }) => {
64
+ const data = { arr: Array.from({ length: 15 }, (_, index) => index) };
65
+ const result = safeStringify(data, createReplacer({ maxArrayLen: 10 }));
66
+ expect(result).to.include('"[length: 15]"');
67
+ });
68
+
69
+ test('maxStringLen truncates long strings (Large story)', ({ expect }) => {
70
+ const data = { str: 'a'.repeat(20) };
71
+ const result = safeStringify(data, createReplacer({ maxStringLen: 10 }));
72
+ expect(result).to.include('"aaaaaaaaaa..."');
73
+ });
74
+
75
+ test('omit skips specified keys', ({ expect }) => {
76
+ const data = { a: 1, secret: 'hide', b: 2 };
77
+ const result = safeStringify(data, createReplacer({ omit: ['secret'] }));
78
+ expect(result).to.include('"a": 1');
79
+ expect(result).to.include('"b": 2');
80
+ expect(result).not.to.include('secret');
81
+ });
82
+
83
+ test('parse parses JSON string values', ({ expect }) => {
84
+ const data = { nested: '{"x":1}' };
85
+ const result = safeStringify(data, createReplacer({ parse: ['nested'] }));
86
+ expect(result).to.include('"x": 1');
87
+ });
88
+
89
+ test('SKIP return value removes key from output', ({ expect }) => {
90
+ const customReplacer = (_key: string, value: unknown) => (_key === 'skip' ? SKIP : value);
91
+ const data = { a: 1, skip: 'hidden' };
92
+ const result = safeStringify(data, customReplacer);
93
+ expect(result).to.include('"a": 1');
94
+ expect(result).not.to.include('skip');
95
+ });
96
+ });
@@ -0,0 +1,153 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export const SKIP = Object.freeze({});
6
+
7
+ /**
8
+ * JSON.stringify replacer function.
9
+ */
10
+ export type StringifyReplacer = (key: string, value: any) => typeof SKIP | any;
11
+
12
+ /**
13
+ * Safely stringifies an object.
14
+ */
15
+ export function safeStringify(obj: any, filter: StringifyReplacer | undefined = defaultFilter, indent = 2) {
16
+ const seen = new WeakMap<object, string>();
17
+
18
+ // NOTE: Called for the root object with undefined key.
19
+ function replacer(this: any, key: string, value: any) {
20
+ try {
21
+ let path = key;
22
+ if (!key) {
23
+ path = '$';
24
+ if (value != null && typeof value === 'object') {
25
+ seen.set(value, path);
26
+ }
27
+ return value;
28
+ } else if (this) {
29
+ const parentPath = seen.get(this);
30
+ path = parentPath ? `${parentPath}.${key}` : key;
31
+ }
32
+
33
+ // Null or undefined.
34
+ if (value == null) {
35
+ return value;
36
+ }
37
+
38
+ // Ignore functions.
39
+ if (typeof value === 'function') {
40
+ return undefined;
41
+ }
42
+
43
+ // Ignore exotic objects (non-plain objects like DOM elements, class instances, etc.)
44
+ if (typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value)) {
45
+ return undefined;
46
+ }
47
+
48
+ // Check cycles.
49
+ if (typeof value === 'object' && value !== null) {
50
+ const exists = seen.get(value);
51
+ if (exists) {
52
+ return `[${path} => ${exists}]`;
53
+ }
54
+
55
+ seen.set(value, path);
56
+ }
57
+
58
+ if (filter) {
59
+ const filteredValue = filter?.(key, value);
60
+ if (filteredValue !== undefined) {
61
+ return filteredValue === SKIP ? undefined : filteredValue;
62
+ }
63
+ }
64
+
65
+ return value;
66
+ } catch (error: any) {
67
+ return `ERROR: ${error.message}`;
68
+ }
69
+ }
70
+
71
+ try {
72
+ return JSON.stringify(obj, replacer, indent);
73
+ } catch (error: any) {
74
+ return `ERROR: ${error.message}`;
75
+ }
76
+ }
77
+
78
+ export type CreateReplacerProps = {
79
+ omit?: string[];
80
+ parse?: string[]; // TODO(burdon): Parse JSON value.
81
+ maxDepth?: number;
82
+ maxArrayLen?: number;
83
+ maxStringLen?: number;
84
+ };
85
+
86
+ /**
87
+ * Construct JSON.stringify replacer.
88
+ */
89
+ // TODO(burdon): Change to composite effect.
90
+ export const createReplacer = ({
91
+ omit,
92
+ parse,
93
+ maxDepth,
94
+ maxArrayLen,
95
+ maxStringLen,
96
+ }: CreateReplacerProps = {}): StringifyReplacer => {
97
+ let currentDepth = 0;
98
+ const depthMap = new WeakMap<object, number>();
99
+
100
+ return function (this: any, key: string, value: any) {
101
+ // Track depth.
102
+ if (key === '') {
103
+ // Root.
104
+ currentDepth = 0;
105
+ } else if (this && typeof this === 'object') {
106
+ const parentDepth = depthMap.get(this) ?? 0;
107
+ currentDepth = parentDepth + 1;
108
+ }
109
+
110
+ // Skip functions.
111
+ if (typeof value === 'function') {
112
+ return SKIP;
113
+ }
114
+
115
+ // Show array length.
116
+ if (maxArrayLen != null && Array.isArray(value) && value.length > maxArrayLen) {
117
+ return `[length: ${value.length}]`;
118
+ }
119
+
120
+ // Store depth for this object.
121
+ if (value && typeof value === 'object') {
122
+ depthMap.set(value, currentDepth);
123
+
124
+ // Check max depth.
125
+ if (maxDepth != null && currentDepth >= maxDepth) {
126
+ return Array.isArray(value) ? `[{ length: ${value.length} }]` : `{ keys: ${Object.keys(value).length} }`;
127
+ }
128
+ }
129
+
130
+ // Apply other filters.
131
+ if (omit?.includes(key)) {
132
+ return SKIP;
133
+ }
134
+
135
+ // Parse JSON values.
136
+ if (parse?.includes(key) && typeof value === 'string') {
137
+ try {
138
+ return JSON.parse(value);
139
+ } catch {
140
+ return value;
141
+ }
142
+ }
143
+
144
+ // Truncate strings.
145
+ if (maxStringLen != null && typeof value === 'string' && value.length > maxStringLen) {
146
+ return value.slice(0, maxStringLen) + '...';
147
+ }
148
+
149
+ return value;
150
+ };
151
+ };
152
+
153
+ export const defaultFilter: StringifyReplacer = createReplacer();
package/src/sort.test.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, expect, test } from 'vitest';
6
6
 
7
- import { compareMulti, compareScalar, compareObject, compareString } from './sort';
7
+ import { compareMulti, compareObject, compareScalar, compareString } from './sort';
8
8
 
9
9
  const data = [
10
10
  { i: 0, idx: 1, label: 'apple' },
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { trim } from './string';
8
+
9
+ describe('string', () => {
10
+ test('dedent', async ({ expect }) => {
11
+ const text = trim`
12
+ - 1
13
+ - 2
14
+ - 3
15
+ `;
16
+
17
+ expect(text).to.eq('- 1\n- 2\n - 3');
18
+ });
19
+ });
package/src/string.ts CHANGED
@@ -12,3 +12,43 @@ export const capitalize = (str: string): string => {
12
12
 
13
13
  return str.charAt(0).toUpperCase() + str.slice(1);
14
14
  };
15
+
16
+ /**
17
+ * Remove leading space from multi-line strings.
18
+ */
19
+ export function trim(strings: TemplateStringsArray, ...values: any[]) {
20
+ // First, build the raw result with relative indentation.
21
+ const raw = strings.reduce((out, str, i) => {
22
+ out += str;
23
+ if (i < values.length) {
24
+ const match = str.match(/(^|\n)([ \t]*)$/);
25
+ const baseIndent = match ? match[2] : '';
26
+ const val = String(values[i]).replace(/\r?\n/g, '\n' + baseIndent);
27
+ out += val;
28
+ }
29
+ return out;
30
+ }, '');
31
+
32
+ // Split into lines and trim leading/trailing blank lines.
33
+ const lines = raw.split('\n');
34
+
35
+ while (lines.length && !lines[0].trim()) {
36
+ lines.shift();
37
+ }
38
+ while (lines.length && !lines[lines.length - 1].trim()) {
39
+ lines.pop();
40
+ }
41
+
42
+ // Find smallest indent across all non-blank lines.
43
+ const minIndent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0));
44
+
45
+ // Remove that indent from all lines.
46
+ return lines.map((l) => l.slice(minIndent)).join('\n');
47
+ }
48
+
49
+ // From https://stackoverflow.com/a/67243723/2804332
50
+ /**
51
+ * Converts a string to kebab case.
52
+ */
53
+ export const kebabize = (str: string) =>
54
+ str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());