utilful 2.5.3 → 3.0.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/dist/csv.mjs CHANGED
@@ -6,6 +6,7 @@ const CARRIAGE_RETURN = "\r";
6
6
  const ESCAPED_QUOTE = "\"\"";
7
7
  const SPACE = " ";
8
8
  const TAB = " ";
9
+ const BYTE_ORDER_MARK = "";
9
10
  function createCSV(data, columnsOrOptions, maybeOptions = {}) {
10
11
  let columns;
11
12
  let options;
@@ -18,7 +19,7 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
18
19
  }
19
20
  if (columns.length === 0 && data.length === 0) return "";
20
21
  const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
21
- if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
22
+ assertValidCSVDelimiter(delimiter);
22
23
  if (addHeader) {
23
24
  const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
24
25
  if (data.length === 0) return header;
@@ -46,7 +47,7 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
46
47
  */
47
48
  async function* createCSVStream(data, columns, options = {}) {
48
49
  const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
49
- if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
50
+ assertValidCSVDelimiter(delimiter);
50
51
  if (addHeader) yield encodeCSVHeader(columns.map(String), delimiter, quoteAll) + lineEnding;
51
52
  for await (const row of data) yield encodeCSVRow(row, columns, delimiter, quoteAll) + lineEnding;
52
53
  }
@@ -71,6 +72,10 @@ async function createCSVAsync(data, columns, options = {}) {
71
72
  for await (const chunk of createCSVStream(data, columns, options)) chunks.push(chunk);
72
73
  return chunks.join("");
73
74
  }
75
+ function assertValidCSVDelimiter(delimiter) {
76
+ if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
77
+ if (delimiter === DOUBLE_QUOTE || delimiter === NEWLINE || delimiter === CARRIAGE_RETURN) throw new RangeError(`CSV delimiter must not be a quote or line break character, got ${JSON.stringify(delimiter)}`);
78
+ }
74
79
  function encodeCSVHeader(columns, delimiter, quoteAll) {
75
80
  return columns.map((col) => escapeCSVValue(col, {
76
81
  delimiter,
@@ -100,35 +105,54 @@ function escapeCSVValue(value, options = {}) {
100
105
  if (value == null) return "";
101
106
  const coercedValue = String(value);
102
107
  const hasQuote = coercedValue.includes(DOUBLE_QUOTE);
103
- if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) return `${DOUBLE_QUOTE}${hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue}${DOUBLE_QUOTE}`;
108
+ if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) {
109
+ const escaped = hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue;
110
+ return `${DOUBLE_QUOTE}${escaped}${DOUBLE_QUOTE}`;
111
+ }
104
112
  return coercedValue;
105
113
  }
106
114
  var CSVParserCore = class {
107
- delimiter;
108
- trim;
109
- strict;
110
- onRow;
111
- currentRow = [];
112
- currentRowQuotedFlags = [];
113
- currentField = "";
114
- inQuotes = false;
115
- isFieldQuoted = false;
116
- currentRowNumber = 1;
117
- headerRaw;
118
- headerQuotedFlags;
119
- headers;
120
115
  constructor(options, onRow) {
121
- const { delimiter = COMMA, trim = true, strict = true } = options;
122
- if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
116
+ this.currentRow = [];
117
+ this.currentField = "";
118
+ this.inQuotes = false;
119
+ this.isFieldQuoted = false;
120
+ this.currentRowNumber = 1;
121
+ this.isAtInputStart = true;
122
+ this.pendingChunkTail = "";
123
+ const { delimiter = COMMA, trim = false, strict = true } = options;
124
+ assertValidCSVDelimiter(delimiter);
123
125
  this.delimiter = delimiter;
124
126
  this.trim = trim;
125
127
  this.strict = strict;
126
128
  this.onRow = onRow;
127
129
  }
128
130
  push(chunk) {
129
- for (let i = 0; i < chunk.length; i++) {
130
- const character = chunk[i];
131
- const nextCharacter = i + 1 < chunk.length ? chunk[i + 1] : "";
131
+ let pendingText = this.pendingChunkTail + chunk;
132
+ this.pendingChunkTail = "";
133
+ if (this.isAtInputStart && pendingText.length > 0) {
134
+ if (pendingText[0] === BYTE_ORDER_MARK) pendingText = pendingText.slice(1);
135
+ this.isAtInputStart = false;
136
+ }
137
+ let holdbackIndex = pendingText.length;
138
+ while (holdbackIndex > 0 && pendingText[holdbackIndex - 1] === DOUBLE_QUOTE) holdbackIndex--;
139
+ if (holdbackIndex === pendingText.length && holdbackIndex > 0 && pendingText[holdbackIndex - 1] === CARRIAGE_RETURN) holdbackIndex--;
140
+ this.pendingChunkTail = pendingText.slice(holdbackIndex);
141
+ this.consume(pendingText.slice(0, holdbackIndex));
142
+ }
143
+ finish() {
144
+ if (this.pendingChunkTail.length > 0) {
145
+ const chunkTail = this.pendingChunkTail;
146
+ this.pendingChunkTail = "";
147
+ this.consume(chunkTail);
148
+ }
149
+ if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
150
+ if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
151
+ }
152
+ consume(text) {
153
+ for (let i = 0; i < text.length; i++) {
154
+ const character = text[i];
155
+ const nextCharacter = i + 1 < text.length ? text[i + 1] : "";
132
156
  if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
133
157
  if (character === DOUBLE_QUOTE) if (this.currentField.length === 0 && !this.inQuotes) {
134
158
  this.inQuotes = true;
@@ -145,74 +169,52 @@ var CSVParserCore = class {
145
169
  } else this.currentField += character;
146
170
  }
147
171
  }
148
- finish() {
149
- if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
150
- if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
151
- }
152
172
  appendField() {
153
- this.currentRow.push(this.currentField);
154
- this.currentRowQuotedFlags.push(this.isFieldQuoted);
173
+ const fieldValue = this.trim && !this.isFieldQuoted ? this.currentField.trim() : this.currentField;
174
+ this.currentRow.push(fieldValue);
155
175
  this.currentField = "";
156
176
  this.isFieldQuoted = false;
157
177
  }
158
178
  appendRow() {
159
179
  this.appendField();
160
- if (!this.headerRaw) {
161
- this.headerRaw = this.currentRow;
162
- this.headerQuotedFlags = this.currentRowQuotedFlags;
163
- this.processHeaderRow();
164
- } else this.processDataRow(this.currentRow, this.currentRowQuotedFlags);
180
+ if (this.headers) this.processDataRow(this.currentRow, this.headers);
181
+ else this.processHeaderRow(this.currentRow);
165
182
  this.currentRow = [];
166
- this.currentRowQuotedFlags = [];
167
183
  this.currentRowNumber++;
168
184
  }
169
- processHeaderRow() {
170
- const headerRow = this.headerRaw;
171
- const headerQuotedFlags = this.headerQuotedFlags ?? [];
172
- const headers = this.trim ? headerRow.map((h, i) => headerQuotedFlags[i] ? h : h.trim()) : headerRow;
173
- if (headers.filter((h) => h.length === 0).length > 0) {
174
- const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
175
- throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
176
- }
177
- const headerSet = /* @__PURE__ */ new Set();
185
+ processHeaderRow(headers) {
186
+ const emptyHeaderPositions = headers.map((header, index) => header.length === 0 ? index + 1 : -1).filter((position) => position > 0);
187
+ if (emptyHeaderPositions.length > 0) throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${emptyHeaderPositions.join(", ")}`);
188
+ const seenHeaderNames = /* @__PURE__ */ new Set();
178
189
  const duplicateHeaderNames = /* @__PURE__ */ new Set();
179
- for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
180
- else headerSet.add(header);
190
+ for (const header of headers) if (seenHeaderNames.has(header)) duplicateHeaderNames.add(header);
191
+ else seenHeaderNames.add(header);
181
192
  if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
182
193
  this.headers = headers;
183
194
  }
184
- processDataRow(fieldValues, quotedFlags) {
185
- if (!this.headers) throw new Error("CSVParserCore: headers not initialized");
186
- const headers = this.headers;
187
- const isFieldPopulated = this.trim ? (field, wasQuoted) => wasQuoted ? field.length > 0 : field.trim().length > 0 : (field) => field.length > 0;
188
- const hasMultipleFields = fieldValues.length > 1;
189
- const hasAnyPopulatedField = fieldValues.some((field, idx) => isFieldPopulated(field, quotedFlags[idx] ?? false));
190
- if (!hasMultipleFields && !hasAnyPopulatedField) return;
191
- if (fieldValues.length > headers.length) {
192
- const fieldsExceedingHeaders = fieldValues.slice(headers.length);
193
- const excessQuotedFlags = quotedFlags.slice(headers.length);
194
- const containsNonEmptyOverflow = fieldsExceedingHeaders.some((field, idx) => isFieldPopulated(field, excessQuotedFlags[idx] ?? false));
195
- if (this.strict && containsNonEmptyOverflow) {
196
- const expectedCount = headers.length;
197
- const actualCount = fieldValues.length;
198
- const excessCount = actualCount - expectedCount;
199
- throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
200
- }
195
+ processDataRow(fieldValues, headers) {
196
+ if (fieldValues.length === 1 && fieldValues[0].length === 0) return;
197
+ if (this.strict) {
198
+ if (fieldValues.length > headers.length) {
199
+ if (fieldValues.slice(headers.length).some((fieldValue) => fieldValue.length > 0)) throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${fieldValues.length - headers.length} extra field(s): expected ${headers.length} column(s), found ${fieldValues.length}`);
200
+ } else if (fieldValues.length < headers.length) throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${headers.length - fieldValues.length} missing field(s): expected ${headers.length} column(s), found ${fieldValues.length}`);
201
201
  }
202
- const rowEntries = headers.map((header, columnIndex) => {
203
- const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
204
- const wasQuoted = quotedFlags[columnIndex] ?? false;
205
- return [header, this.trim && !wasQuoted ? untrimmedValue.trim() : untrimmedValue];
206
- });
207
- const rowObject = Object.fromEntries(rowEntries);
208
- this.onRow(rowObject);
202
+ const rowEntries = headers.map((header, columnIndex) => [header, fieldValues[columnIndex] ?? ""]);
203
+ this.onRow(Object.fromEntries(rowEntries));
209
204
  }
210
205
  };
211
206
  /**
212
207
  * Parses a comma-separated values (CSV) string into an array of objects.
213
208
  *
214
209
  * @remarks
215
- * The first row of the CSV string is used as the header row.
210
+ * The first row of the CSV string is used as the header row. A leading
211
+ * UTF-8 byte order mark is stripped.
212
+ *
213
+ * Parsing tolerances (lenient deviations from RFC 4180):
214
+ * - LF, CR, and CRLF line endings are all accepted
215
+ * - Whitespace between a closing quote and the next delimiter or line break is ignored
216
+ * - A field is only treated as quoted if it starts with a quote; quotes inside
217
+ * unquoted fields are kept as literal characters
216
218
  *
217
219
  * @example
218
220
  * const csv = `name,age
@@ -260,24 +262,6 @@ async function* parseCSVStream(chunks, options = {}) {
260
262
  while (queue.length > 0) yield queue.shift();
261
263
  }
262
264
  /**
263
- * Parses CSV data from an async iterable or iterable of lines.
264
- *
265
- * @remarks
266
- * This is a convenience wrapper around `parseCSVStream` that treats each line
267
- * as a chunk. Note that lines do not necessarily correspond to CSV rows due to
268
- * quoted fields containing newlines. The parser handles this correctly.
269
- *
270
- * @example
271
- * const lines = ['name,age', 'John,30', 'Jane,25']
272
- *
273
- * for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
274
- * console.log(row)
275
- * }
276
- */
277
- async function* parseCSVFromLines(lines, options) {
278
- yield* parseCSVStream(lines, options);
279
- }
280
- /**
281
265
  * Infers column names from data by collecting the union of keys
282
266
  * across all rows in first-seen order.
283
267
  */
@@ -292,4 +276,4 @@ function inferColumns(rows) {
292
276
  return columns;
293
277
  }
294
278
  //#endregion
295
- export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
279
+ export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVStream };
package/dist/defu.d.mts CHANGED
@@ -2,13 +2,19 @@
2
2
  type PlainObject = Record<PropertyKey, any>;
3
3
  type DefuMerger<T extends PlainObject = PlainObject> = (target: T, key: PropertyKey, value: any, namespace: string) => boolean | void;
4
4
  /**
5
- * Defu function type that accepts a source and multiple defaults
6
- */
7
- type DefuFn = <T extends PlainObject>(source: T, ...defaults: PlainObject[]) => T;
5
+ * Deeply merged result type of a source object over a list of defaults.
6
+ */
7
+ type Defu<Source, Defaults extends any[]> = Defaults extends [infer First, ...infer Rest] ? Defu<MergedObject<Source, First>, Rest> : Source;
8
+ type MergedObject<Source, Defaults> = Source extends PlainObject ? Defaults extends PlainObject ? { [Key in keyof Source | keyof Defaults]: MergedValue<Key extends keyof Source ? Source[Key] : undefined, Key extends keyof Defaults ? Defaults[Key] : undefined>; } : Source : Source;
9
+ type MergedValue<SourceValue, DefaultValue> = SourceValue extends null | undefined ? DefaultValue : SourceValue extends any[] ? DefaultValue extends any[] ? Array<SourceValue[number] | DefaultValue[number]> : SourceValue : SourceValue extends ((...args: any[]) => any) ? SourceValue : SourceValue extends PlainObject ? MergedObject<SourceValue, DefaultValue> : SourceValue;
8
10
  /**
9
- * Create a defu function with optional custom merger
10
- */
11
+ * Defu function type that accepts a source and multiple defaults
12
+ */
13
+ type DefuFn = <Source extends PlainObject, Defaults extends PlainObject[]>(source: Source, ...defaults: Defaults) => Defu<Source, Defaults>;
14
+ /**
15
+ * Create a defu function with optional custom merger
16
+ */
11
17
  declare function createDefu(merger?: DefuMerger): DefuFn;
12
18
  declare const defu: DefuFn;
13
19
  //#endregion
14
- export { DefuFn, DefuMerger, createDefu, defu };
20
+ export { Defu, DefuFn, DefuMerger, createDefu, defu };
package/dist/defu.mjs CHANGED
@@ -3,9 +3,9 @@
3
3
  * Create a defu function with optional custom merger
4
4
  */
5
5
  function createDefu(merger) {
6
- return (source, ...defaults) => {
7
- return defaults.reduce((acc, current) => _defu(acc, current ?? {}, "", merger), source ?? {});
8
- };
6
+ return ((source, ...defaults) => {
7
+ return defaults.reduce((mergedResult, currentDefaults) => _defu(mergedResult, currentDefaults ?? {}, "", merger), source ?? {});
8
+ });
9
9
  }
10
10
  const defu = createDefu();
11
11
  function _defu(source, defaults, namespace = "", merger) {
@@ -15,11 +15,11 @@ interface Emitter<Events extends Record<EventType, unknown>> {
15
15
  emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
16
16
  }
17
17
  /**
18
- * Simple functional event emitter / pubsub.
19
- *
20
- * @remarks Ported from `mitt`.
21
- * @see https://github.com/developit/mitt
22
- */
18
+ * Simple functional event emitter / pubsub.
19
+ *
20
+ * @remarks Ported from `mitt`.
21
+ * @see https://github.com/developit/mitt
22
+ */
23
23
  declare function createEmitter<Events extends Record<EventType, unknown>>(events?: EventHandlerMap<Events>): Emitter<Events>;
24
24
  //#endregion
25
25
  export { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter };
package/dist/emitter.mjs CHANGED
@@ -8,17 +8,42 @@
8
8
  function createEmitter(events) {
9
9
  events ||= /* @__PURE__ */ new Map();
10
10
  return {
11
+ /**
12
+ * A Map of event names to registered handler functions.
13
+ */
11
14
  events,
15
+ /**
16
+ * Register an event handler for the given type.
17
+ *
18
+ * @memberOf createEmitter
19
+ */
12
20
  on(type, handler) {
13
21
  const handlers = events.get(type);
14
22
  if (handlers) handlers.push(handler);
15
23
  else events.set(type, [handler]);
16
24
  },
25
+ /**
26
+ * Remove an event handler for the given type.
27
+ *
28
+ * @remarks
29
+ * If `handler` is omitted, all handlers of the given type are removed.
30
+ *
31
+ * @memberOf createEmitter
32
+ */
17
33
  off(type, handler) {
18
34
  const handlers = events.get(type);
19
35
  if (handlers) if (handler) handlers.splice(handlers.indexOf(handler) >>> 0, 1);
20
36
  else events.set(type, []);
21
37
  },
38
+ /**
39
+ * Invoke all handlers for the given type.
40
+ *
41
+ * @remarks
42
+ * If present, `'*'` handlers are invoked after type-matched handlers.
43
+ * Manually firing '*' handlers is not supported.
44
+ *
45
+ * @memberOf createEmitter
46
+ */
22
47
  emit(type, evt) {
23
48
  let handlers = events.get(type);
24
49
  if (handlers) for (const handler of [...handlers]) handler(evt);
package/dist/index.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { MaybeArray, toArray } from "./array.mjs";
2
- import { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
3
- import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
2
+ import { CSVCreateOptions, CSVParseOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVStream } from "./csv.mjs";
3
+ import { Defu, DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
4
4
  import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
5
- import { cloneJSON, tryParseJSON } from "./json.mjs";
5
+ import { tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
8
8
  import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
9
  import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
- import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
10
+ import { generateRandomId, template } from "./string.mjs";
11
11
  import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
12
- export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, TEMPLATE_PLACEHOLDER_RE, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
12
+ export { AutocompletableString, BrandedType, CSVCreateOptions, CSVParseOptions, CSVRow, Defu, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { toArray } from "./array.mjs";
2
- import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
2
+ import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVStream } from "./csv.mjs";
3
3
  import { createDefu, defu } from "./defu.mjs";
4
4
  import { createEmitter } from "./emitter.mjs";
5
- import { cloneJSON, tryParseJSON } from "./json.mjs";
5
+ import { tryParseJSON } from "./json.mjs";
6
6
  import { interopDefault } from "./module.mjs";
7
7
  import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
8
8
  import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
9
9
  import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
10
- import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
11
- export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
10
+ import { generateRandomId, template } from "./string.mjs";
11
+ export { Err, Ok, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
package/dist/json.d.mts CHANGED
@@ -1,17 +1,10 @@
1
1
  //#region src/json.d.ts
2
2
  /**
3
- * Type-safe wrapper around `JSON.stringify`.
4
- *
5
- * @remarks
6
- * Falls back to the original value if the JSON serialization fails or the value is not a string.
7
- */
3
+ * Type-safe wrapper around `JSON.parse`.
4
+ *
5
+ * @remarks
6
+ * Falls back to the original value if parsing fails or the value is not a string.
7
+ */
8
8
  declare function tryParseJSON<T = unknown>(value: unknown): T;
9
- /**
10
- * Clones the given JSON value.
11
- *
12
- * @remarks
13
- * The value must not contain circular references as JSON does not support them. It also must contain JSON serializable values.
14
- */
15
- declare function cloneJSON<T>(value: T): T;
16
9
  //#endregion
17
- export { cloneJSON, tryParseJSON };
10
+ export { tryParseJSON };
package/dist/json.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  //#region src/json.ts
2
2
  /**
3
- * Type-safe wrapper around `JSON.stringify`.
3
+ * Type-safe wrapper around `JSON.parse`.
4
4
  *
5
5
  * @remarks
6
- * Falls back to the original value if the JSON serialization fails or the value is not a string.
6
+ * Falls back to the original value if parsing fails or the value is not a string.
7
7
  */
8
8
  function tryParseJSON(value) {
9
9
  if (typeof value !== "string") return value;
@@ -13,21 +13,5 @@ function tryParseJSON(value) {
13
13
  return value;
14
14
  }
15
15
  }
16
- /**
17
- * Clones the given JSON value.
18
- *
19
- * @remarks
20
- * The value must not contain circular references as JSON does not support them. It also must contain JSON serializable values.
21
- */
22
- function cloneJSON(value) {
23
- if (typeof value !== "object" || value === null) return value;
24
- if (Array.isArray(value)) return value.map((element) => typeof element !== "object" || element === null ? element : cloneJSON(element));
25
- const result = {};
26
- for (const key in value) {
27
- const propertyValue = value[key];
28
- result[key] = typeof propertyValue !== "object" || propertyValue === null ? propertyValue : cloneJSON(propertyValue);
29
- }
30
- return result;
31
- }
32
16
  //#endregion
33
- export { cloneJSON, tryParseJSON };
17
+ export { tryParseJSON };
package/dist/module.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  //#region src/module.d.ts
2
2
  /**
3
- * Interop helper for default exports.
4
- *
5
- * @example
6
- * const mod = await interopDefault(import('./module.js'))
7
- */
3
+ * Interop helper for default exports.
4
+ *
5
+ * @example
6
+ * const mod = await interopDefault(import('./module.js'))
7
+ */
8
8
  declare function interopDefault<T>(m: T | Promise<T>): Promise<T extends {
9
9
  default: infer U;
10
10
  } ? U : T>;
package/dist/module.mjs CHANGED
@@ -7,7 +7,8 @@
7
7
  */
8
8
  async function interopDefault(m) {
9
9
  const resolved = await m;
10
- return resolved.default || resolved;
10
+ if (resolved != null && (typeof resolved === "object" || typeof resolved === "function") && "default" in resolved) return resolved.default;
11
+ return resolved;
11
12
  }
12
13
  //#endregion
13
14
  export { interopDefault };
package/dist/object.d.mts CHANGED
@@ -1,36 +1,40 @@
1
1
  //#region src/object.d.ts
2
2
  /**
3
- * A simple general purpose memoizer utility.
4
- * - Lazily computes a value when accessed
5
- * - Auto-caches the result by overwriting the getter
6
- *
7
- * @remarks
8
- * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
9
- *
10
- * @example
11
- * const myValue = lazy(() => 'Hello, World!')
12
- * console.log(myValue.value) // Computes value, overwrites getter
13
- * console.log(myValue.value) // Returns cached value
14
- * console.log(myValue.value) // Returns cached value
15
- */
3
+ * A simple general purpose memoizer utility.
4
+ * - Lazily computes a value when accessed
5
+ * - Auto-caches the result by overwriting the getter
6
+ *
7
+ * @remarks
8
+ * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
9
+ *
10
+ * @example
11
+ * const myValue = memoize(() => 'Hello, World!')
12
+ * console.log(myValue.value) // Computes value, overwrites getter
13
+ * console.log(myValue.value) // Returns cached value
14
+ * console.log(myValue.value) // Returns cached value
15
+ */
16
16
  declare function memoize<T>(getter: () => T): {
17
17
  value: T;
18
18
  };
19
19
  /**
20
- * Strictly typed `Object.keys`.
21
- */
22
- declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${keyof T & (string | number | boolean | null | undefined)}`>;
20
+ * Strictly typed `Object.keys`.
21
+ */
22
+ declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${Extract<keyof T, string | number>}`>;
23
23
  /**
24
- * Strictly typed `Object.entries`.
25
- */
24
+ * Strictly typed `Object.entries`.
25
+ */
26
26
  declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>;
27
27
  /**
28
- * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays.
29
- */
28
+ * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays (including arrays nested inside arrays).
29
+ */
30
30
  declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void;
31
31
  /**
32
- * Checks if a value is a plain object.
33
- */
32
+ * Checks if a value is an object with the plain `[object Object]` tag.
33
+ *
34
+ * @remarks
35
+ * Returns `true` for object literals, class instances, and `null`-prototype
36
+ * objects – unlike stricter plain-object checks that reject class instances.
37
+ */
34
38
  declare function isObject(value: unknown): value is Record<any, any>;
35
39
  //#endregion
36
40
  export { deepApply, isObject, memoize, objectEntries, objectKeys };
package/dist/object.mjs CHANGED
@@ -5,10 +5,10 @@
5
5
  * - Auto-caches the result by overwriting the getter
6
6
  *
7
7
  * @remarks
8
- * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
8
+ * Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
9
9
  *
10
10
  * @example
11
- * const myValue = lazy(() => 'Hello, World!')
11
+ * const myValue = memoize(() => 'Hello, World!')
12
12
  * console.log(myValue.value) // Computes value, overwrites getter
13
13
  * console.log(myValue.value) // Returns cached value
14
14
  * console.log(myValue.value) // Returns cached value
@@ -33,18 +33,24 @@ function objectEntries(obj) {
33
33
  return Object.entries(obj);
34
34
  }
35
35
  /**
36
- * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays.
36
+ * Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays (including arrays nested inside arrays).
37
37
  */
38
38
  function deepApply(data, callback) {
39
39
  for (const [key, value] of Object.entries(data)) {
40
40
  callback(data, key, value);
41
- if (Array.isArray(value)) {
42
- for (const element of value) if (isObject(element)) deepApply(element, callback);
43
- } else if (isObject(value)) deepApply(value, callback);
41
+ applyToNestedValue(value, callback);
44
42
  }
45
43
  }
44
+ function applyToNestedValue(value, callback) {
45
+ if (Array.isArray(value)) for (const element of value) applyToNestedValue(element, callback);
46
+ else if (isObject(value)) deepApply(value, callback);
47
+ }
46
48
  /**
47
- * Checks if a value is a plain object.
49
+ * Checks if a value is an object with the plain `[object Object]` tag.
50
+ *
51
+ * @remarks
52
+ * Returns `true` for object literals, class instances, and `null`-prototype
53
+ * objects – unlike stricter plain-object checks that reject class instances.
48
54
  */
49
55
  function isObject(value) {
50
56
  return Object.prototype.toString.call(value) === "[object Object]";
package/dist/path.d.mts CHANGED
@@ -2,42 +2,46 @@
2
2
  type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined;
3
3
  type QueryObject = Record<string, QueryValue | QueryValue[]>;
4
4
  /**
5
- * Removes the leading slash from the given path if it has one.
6
- */
5
+ * Removes the leading slash from the given path if it has one.
6
+ */
7
7
  declare function withoutLeadingSlash(path?: string): string;
8
8
  /**
9
- * Adds a leading slash to the given path if it does not already have one.
10
- */
9
+ * Adds a leading slash to the given path if it does not already have one.
10
+ */
11
11
  declare function withLeadingSlash(path?: string): string;
12
12
  /**
13
- * Removes the trailing slash from the given path if it has one,
14
- * preserving query strings and hash fragments.
15
- */
13
+ * Removes the trailing slash from the given path if it has one,
14
+ * preserving query strings and hash fragments.
15
+ */
16
16
  declare function withoutTrailingSlash(path?: string): string;
17
17
  /**
18
- * Adds a trailing slash to the given path if it does not already have one,
19
- * preserving query strings and hash fragments.
20
- */
18
+ * Adds a trailing slash to the given path if it does not already have one,
19
+ * preserving query strings and hash fragments.
20
+ */
21
21
  declare function withTrailingSlash(path?: string): string;
22
22
  /**
23
- * Joins the given URL path segments, ensuring that there is only one slash between them.
24
- */
23
+ * Joins the given URL path segments, ensuring that there is only one slash between them.
24
+ */
25
25
  declare function joinURL(...paths: (string | undefined)[]): string;
26
26
  /**
27
- * Adds the base path to the input path, if it is not already present.
28
- */
27
+ * Adds the base path to the input path, if it is not already present.
28
+ */
29
29
  declare function withBase(input?: string, base?: string): string;
30
30
  /**
31
- * Removes the base path from the input path, if it is present.
32
- */
31
+ * Removes the base path from the input path, if it is present.
32
+ */
33
33
  declare function withoutBase(input?: string, base?: string): string;
34
34
  /**
35
- * Returns the pathname of the given path, which is the path without the query string or hash.
36
- */
35
+ * Returns the pathname of the given path, which is the path without the query string or hash.
36
+ *
37
+ * @remarks
38
+ * Absolute URLs (with a scheme, e.g. `https://example.com/foo`) return the URL's pathname.
39
+ * All other inputs are returned unchanged with the query string and hash removed.
40
+ */
37
41
  declare function getPathname(path?: string): string;
38
42
  /**
39
- * Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
40
- */
43
+ * Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
44
+ */
41
45
  declare function withQuery(input: string, query?: QueryObject): string;
42
46
  //#endregion
43
47
  export { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };