@ucdjs-internal/shared 0.1.1-beta.1

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/index.mjs ADDED
@@ -0,0 +1,914 @@
1
+ import { createDebug } from "obug";
2
+ import { isMSWError } from "@luxass/msw-utils/runtime-guards";
3
+ import { prependLeadingSlash, trimLeadingSlash, trimTrailingSlash } from "@luxass/utils";
4
+ import { hasUCDFolderPath } from "@unicode-utils/core";
5
+ import picomatch from "picomatch";
6
+ import { UCDWellKnownConfigSchema } from "@ucdjs/schemas";
7
+
8
+ //#region src/async/promise-concurrency.ts
9
+ function ensureIsPositiveConcurrency(concurrency, ErrorClazz) {
10
+ if (concurrency !== Number.POSITIVE_INFINITY && !Number.isInteger(concurrency) || typeof concurrency !== "number" || concurrency < 1) throw new ErrorClazz("Concurrency must be a positive integer");
11
+ }
12
+ /**
13
+ * Creates a concurrency limiter that restricts the number of concurrent executions.
14
+ *
15
+ * @param {number} concurrency - The maximum number of concurrent executions allowed.
16
+ * @returns {} A function that wraps any function to enforce the concurrency limit
17
+ * @throws {Error} When concurrency is not a positive integer
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { createConcurrencyLimiter } from "@ucdjs-internal/shared";
22
+ *
23
+ * const limiter = createConcurrencyLimiter(2);
24
+ *
25
+ * // Only 2 of these will run concurrently
26
+ * const results = await Promise.all([
27
+ * limiter(fetchData, "url1"),
28
+ * limiter(fetchData, "url2"),
29
+ * limiter(fetchData, "url3"),
30
+ * limiter(fetchData, "url4")
31
+ * ]);
32
+ * ```
33
+ */
34
+ function createConcurrencyLimiter(concurrency) {
35
+ ensureIsPositiveConcurrency(concurrency, Error);
36
+ let activeTasks = 0;
37
+ let head;
38
+ let tail;
39
+ function finish() {
40
+ activeTasks--;
41
+ if (head) {
42
+ head[0]();
43
+ head = head[1];
44
+ tail = head && tail;
45
+ }
46
+ }
47
+ return (fn, ...args) => {
48
+ return new Promise((resolve) => {
49
+ if (activeTasks++ < concurrency) resolve();
50
+ else if (tail) tail = tail[1] = [resolve];
51
+ else head = tail = [resolve];
52
+ }).then(() => fn(...args)).finally(finish);
53
+ };
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/debugger.ts
58
+ function createDebugger(namespace) {
59
+ const debug = createDebug(namespace);
60
+ if (debug.enabled) return debug;
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/async/try-catch.ts
65
+ const debug$1 = createDebugger("ucdjs:shared:try-catch");
66
+ function wrapTry(operation) {
67
+ debug$1?.("wrapTry: called", { operationType: typeof operation === "function" ? "function" : "promise" });
68
+ try {
69
+ const result = typeof operation === "function" ? operation() : operation;
70
+ if (isPromise(result)) {
71
+ debug$1?.("wrapTry: executing async operation");
72
+ return Promise.resolve(result).then((data) => {
73
+ debug$1?.("wrapTry: async operation succeeded", {
74
+ hasData: data != null,
75
+ dataType: typeof data
76
+ });
77
+ return onSuccess(data);
78
+ }).catch((error) => {
79
+ debug$1?.("wrapTry: async operation failed", {
80
+ errorName: error instanceof Error ? error.name : "Unknown",
81
+ errorMessage: error instanceof Error ? error.message : String(error)
82
+ });
83
+ return onFailure(error);
84
+ });
85
+ }
86
+ debug$1?.("wrapTry: sync operation succeeded", {
87
+ hasData: result != null,
88
+ dataType: typeof result
89
+ });
90
+ return onSuccess(result);
91
+ } catch (error) {
92
+ debug$1?.("wrapTry: sync operation failed", {
93
+ errorName: error instanceof Error ? error.name : "Unknown",
94
+ errorMessage: error instanceof Error ? error.message : String(error)
95
+ });
96
+ return onFailure(error);
97
+ }
98
+ }
99
+ function onSuccess(value) {
100
+ return [value, null];
101
+ }
102
+ function onFailure(error) {
103
+ return [null, error instanceof Error ? error : new Error(String(error))];
104
+ }
105
+ function isPromise(value) {
106
+ return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
107
+ }
108
+ function tryOr(config) {
109
+ const tryType = typeof config.try === "function" ? "function" : "promise";
110
+ debug$1?.("tryOr: called", { tryType });
111
+ try {
112
+ const tryResult = typeof config.try === "function" ? config.try() : config.try;
113
+ if (isPromise(tryResult)) {
114
+ debug$1?.("tryOr: executing async try");
115
+ return Promise.resolve(tryResult).then((data) => {
116
+ debug$1?.("tryOr: async try succeeded", {
117
+ hasData: data != null,
118
+ dataType: typeof data
119
+ });
120
+ return data;
121
+ }).catch((error) => {
122
+ debug$1?.("tryOr: async try failed, invoking error handler", {
123
+ errorName: error instanceof Error ? error.name : "Unknown",
124
+ errorMessage: error instanceof Error ? error.message : String(error)
125
+ });
126
+ const errResult = config.err(error);
127
+ if (isPromise(errResult)) {
128
+ debug$1?.("tryOr: error handler returned promise");
129
+ return errResult;
130
+ }
131
+ debug$1?.("tryOr: error handler returned sync value");
132
+ return errResult;
133
+ });
134
+ }
135
+ debug$1?.("tryOr: sync try succeeded", {
136
+ hasData: tryResult != null,
137
+ dataType: typeof tryResult
138
+ });
139
+ return tryResult;
140
+ } catch (err) {
141
+ debug$1?.("tryOr: sync try failed, invoking error handler", {
142
+ errorName: err instanceof Error ? err.name : "Unknown",
143
+ errorMessage: err instanceof Error ? err.message : String(err)
144
+ });
145
+ const errResult = config.err(err);
146
+ if (isPromise(errResult)) {
147
+ debug$1?.("tryOr: error handler returned promise");
148
+ return errResult;
149
+ }
150
+ debug$1?.("tryOr: error handler returned sync value");
151
+ return errResult;
152
+ }
153
+ }
154
+
155
+ //#endregion
156
+ //#region src/json.ts
157
+ const debug = createDebugger("ucdjs:shared:json");
158
+ /**
159
+ * Safely parses a JSON string into an object of type T.
160
+ * Returns null if the parsing fails.
161
+ *
162
+ * @template T - The expected type of the parsed JSON
163
+ * @param {string} content - The JSON string to parse
164
+ * @returns {T | null} The parsed object of type T or null if parsing fails
165
+ */
166
+ function safeJsonParse(content) {
167
+ try {
168
+ return JSON.parse(content);
169
+ } catch (err) {
170
+ debug?.("Failed to parse JSON", {
171
+ content,
172
+ error: err instanceof Error ? err.message : String(err)
173
+ });
174
+ return null;
175
+ }
176
+ }
177
+
178
+ //#endregion
179
+ //#region src/fetch/error.ts
180
+ var FetchError = class FetchError extends Error {
181
+ request;
182
+ options;
183
+ response;
184
+ data;
185
+ status;
186
+ statusText;
187
+ constructor(message, opts) {
188
+ super(message, opts);
189
+ this.name = "FetchError";
190
+ if (opts?.cause && !this.cause) this.cause = opts.cause;
191
+ }
192
+ static from(ctx) {
193
+ const errorMessage = ctx.error?.message || ctx.error?.toString() || "";
194
+ const method = ctx.request?.method || ctx.options?.method || "GET";
195
+ const url = ctx.request?.url || String(ctx.request) || "/";
196
+ const fetchError = new FetchError(`${`[${method}] ${JSON.stringify(url)}`}: ${ctx.response ? `${ctx.response.status} ${ctx.response.statusText}` : "<no response>"}${errorMessage ? ` ${errorMessage}` : ""}`, ctx.error ? { cause: ctx.error } : void 0);
197
+ Object.assign(fetchError, {
198
+ request: ctx.request,
199
+ options: ctx.options,
200
+ response: ctx.response,
201
+ data: ctx.response?.data,
202
+ status: ctx.response?.status,
203
+ statusText: ctx.response?.statusText
204
+ });
205
+ return fetchError;
206
+ }
207
+ };
208
+ var FetchSchemaValidationError = class extends FetchError {
209
+ issues;
210
+ constructor(message, opts) {
211
+ super(message, opts);
212
+ this.name = "FetchSchemaValidationError";
213
+ if (opts?.issues !== void 0) this.issues = opts.issues;
214
+ }
215
+ };
216
+
217
+ //#endregion
218
+ //#region src/fetch/utils.ts
219
+ const HTTP_METHODS_WITH_PAYLOADS = new Set([
220
+ "PATCH",
221
+ "POST",
222
+ "PUT",
223
+ "DELETE"
224
+ ]);
225
+ function isPayloadMethod(method) {
226
+ return HTTP_METHODS_WITH_PAYLOADS.has(method?.toUpperCase() || "");
227
+ }
228
+ function isJSONSerializable(value) {
229
+ if (value === void 0) return false;
230
+ if (value === null) return true;
231
+ const t = typeof value;
232
+ if (t === "string" || t === "number" || t === "boolean") return true;
233
+ if (t !== "object") return false;
234
+ if (Array.isArray(value)) return true;
235
+ if (value && value.buffer) return false;
236
+ if (value instanceof FormData || value instanceof URLSearchParams) return false;
237
+ return !!value && value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function";
238
+ }
239
+ const TEXT_TYPES = new Set([
240
+ "image/svg",
241
+ "application/xml",
242
+ "application/xhtml",
243
+ "application/html"
244
+ ]);
245
+ const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(?:;.+)?$/i;
246
+ function detectResponseType(_contentType = "") {
247
+ if (!_contentType) return "json";
248
+ const contentType = _contentType.split(";").shift() || "";
249
+ if (JSON_RE.test(contentType)) return "json";
250
+ if (contentType === "text/event-stream") return "stream";
251
+ if (TEXT_TYPES.has(contentType) || contentType.startsWith("text/")) return "text";
252
+ return "blob";
253
+ }
254
+
255
+ //#endregion
256
+ //#region src/fetch/fetch.ts
257
+ const DEFAULT_RETRY_STATUS_CODES = new Set([
258
+ 408,
259
+ 409,
260
+ 425,
261
+ 429,
262
+ 500,
263
+ 502,
264
+ 503,
265
+ 504
266
+ ]);
267
+ const nullBodyResponses = new Set([
268
+ 101,
269
+ 204,
270
+ 205,
271
+ 304
272
+ ]);
273
+ function createCustomFetch() {
274
+ async function handleError(context) {
275
+ const isAbort = context.error && context.error.name === "AbortError" && !context.options.timeout || false;
276
+ const isMSW = context.error && isMSWError(context.error);
277
+ if (context.options.retry !== false && !isAbort && !isMSW) {
278
+ let retries;
279
+ if (typeof context.options.retry === "number") retries = context.options.retry;
280
+ else retries = isPayloadMethod(context.options.method) ? 0 : 1;
281
+ const responseCode = context.response && context.response.status || 500;
282
+ if (retries > 0 && (Array.isArray(context.options.retryStatusCodes) ? context.options.retryStatusCodes.includes(responseCode) : DEFAULT_RETRY_STATUS_CODES.has(responseCode))) {
283
+ const retryDelay = typeof context.options.retryDelay === "function" ? context.options.retryDelay({
284
+ ...context,
285
+ retryAttempt: context.options.retry - retries + 1
286
+ }) : context.options.retryDelay || 0;
287
+ if (retryDelay > 0) await new Promise((resolve) => setTimeout(resolve, retryDelay));
288
+ return executeFetch(context.request, {
289
+ ...context.options,
290
+ retry: retries - 1
291
+ });
292
+ }
293
+ }
294
+ let error;
295
+ if (context.error instanceof FetchError) {
296
+ error = context.error;
297
+ Object.assign(error, {
298
+ request: error.request ?? context.request,
299
+ options: error.options ?? context.options,
300
+ response: error.response ?? context.response,
301
+ data: error.data ?? context.response?.data,
302
+ status: error.status ?? context.response?.status,
303
+ statusText: error.statusText ?? context.response?.statusText
304
+ });
305
+ } else error = FetchError.from(context);
306
+ if (Error.captureStackTrace) Error.captureStackTrace(error, executeFetch);
307
+ throw error;
308
+ }
309
+ async function executeFetch(_request, _options = {}) {
310
+ const context = {
311
+ request: _request,
312
+ options: {
313
+ ..._options,
314
+ headers: new Headers(_options.headers ?? _request?.headers)
315
+ },
316
+ response: void 0,
317
+ error: void 0
318
+ };
319
+ if (context.options.method) context.options.method = context.options.method.toUpperCase();
320
+ if (context.options.body && isPayloadMethod(context.options.method)) {
321
+ if (isJSONSerializable(context.options.body)) {
322
+ const contentType = context.options.headers.get("content-type");
323
+ if (typeof context.options.body !== "string") context.options.body = contentType === "application/x-www-form-urlencoded" ? new URLSearchParams(context.options.body).toString() : JSON.stringify(context.options.body);
324
+ context.options.headers = new Headers(context.options.headers || {});
325
+ if (!contentType) context.options.headers.set("content-type", "application/json");
326
+ if (!context.options.headers.has("accept")) context.options.headers.set("accept", "application/json");
327
+ } else if ("pipeTo" in context.options.body && typeof context.options.body.pipeTo === "function" || typeof context.options.body.pipe === "function") {
328
+ if (!("duplex" in context.options)) context.options.duplex = "half";
329
+ }
330
+ }
331
+ let abortTimeout;
332
+ if (!context.options.signal && context.options.timeout) {
333
+ const controller = new AbortController();
334
+ abortTimeout = setTimeout(() => {
335
+ const error = /* @__PURE__ */ new Error("[TimeoutError]: The operation was aborted due to timeout");
336
+ error.name = "TimeoutError";
337
+ error.code = 23;
338
+ controller.abort(error);
339
+ }, context.options.timeout);
340
+ context.options.signal = controller.signal;
341
+ }
342
+ try {
343
+ context.response = await fetch(context.request, context.options);
344
+ } catch (error) {
345
+ context.error = error;
346
+ return await handleError(context);
347
+ } finally {
348
+ if (abortTimeout) clearTimeout(abortTimeout);
349
+ }
350
+ if ((context.response.body || context.response._bodyInit) && !nullBodyResponses.has(context.response.status) && context.options.method !== "HEAD") {
351
+ const responseType = context.options.parseAs || detectResponseType(context.response.headers.get("content-type") || "");
352
+ switch (responseType) {
353
+ case "json": {
354
+ const data = await context.response.text();
355
+ context.response.data = safeJsonParse(data) ?? void 0;
356
+ break;
357
+ }
358
+ case "stream":
359
+ context.response.data = context.response.body || context.response._bodyInit;
360
+ break;
361
+ default: context.response.data = await context.response[responseType]();
362
+ }
363
+ if (context.options.schema && context.response.data !== void 0 && context.response.status < 400) {
364
+ const result = await context.options.schema.safeParseAsync(context.response.data);
365
+ if (!result.success) {
366
+ context.error = new FetchSchemaValidationError(`Response validation failed: ${result.error.message}`, {
367
+ cause: result.error,
368
+ issues: result.error.issues
369
+ });
370
+ return await handleError(context);
371
+ }
372
+ context.response.data = result.data;
373
+ }
374
+ }
375
+ if (context.response.status >= 400 && context.response.status < 600) return await handleError(context);
376
+ return context.response;
377
+ }
378
+ async function safeFetch(request, options) {
379
+ try {
380
+ const response = await executeFetch(request, options);
381
+ return {
382
+ data: response.data ?? null,
383
+ response,
384
+ error: null
385
+ };
386
+ } catch (err) {
387
+ if (!(err instanceof FetchError)) return {
388
+ data: null,
389
+ error: FetchError.from({
390
+ request,
391
+ options: {
392
+ ...options,
393
+ headers: new Headers(options?.headers || {})
394
+ },
395
+ response: void 0,
396
+ error: err
397
+ }),
398
+ response: void 0
399
+ };
400
+ return {
401
+ data: null,
402
+ error: err,
403
+ response: err.response
404
+ };
405
+ }
406
+ }
407
+ const customFetch = executeFetch;
408
+ customFetch.safe = safeFetch;
409
+ return customFetch;
410
+ }
411
+ const customFetch = createCustomFetch();
412
+
413
+ //#endregion
414
+ //#region src/files.ts
415
+ /**
416
+ * Normalizes an API file-tree path to a version-relative path suitable for filtering.
417
+ *
418
+ * This strips:
419
+ * - Leading/trailing slashes
420
+ * - Version prefix (e.g., "16.0.0/")
421
+ * - "ucd/" prefix for versions that have it
422
+ *
423
+ * @param {string} version - The Unicode version string
424
+ * @param {string} rawPath - The raw path from the API file tree (e.g., "/16.0.0/ucd/Blocks.txt")
425
+ * @returns {string} The normalized path (e.g., "Blocks.txt")
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt");
430
+ * // Returns: "Blocks.txt"
431
+ *
432
+ * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/GraphemeBreakProperty.txt");
433
+ * // Returns: "auxiliary/GraphemeBreakProperty.txt"
434
+ * ```
435
+ */
436
+ function normalizePathForFiltering(version, rawPath) {
437
+ let path = trimTrailingSlash(trimLeadingSlash(rawPath));
438
+ const versionPrefix = `${version}/`;
439
+ if (path.startsWith(versionPrefix)) path = path.slice(versionPrefix.length);
440
+ if (hasUCDFolderPath(version) && path.startsWith("ucd/")) path = path.slice(4);
441
+ return path;
442
+ }
443
+ /**
444
+ * Creates a normalized view of a file tree for filtering purposes.
445
+ *
446
+ * This recursively maps all `path` properties to version-relative paths,
447
+ * so that filter patterns like "Blocks.txt" or "auxiliary/**" will match
448
+ * against paths like "/16.0.0/ucd/Blocks.txt".
449
+ *
450
+ * @template {UnicodeFileTreeNodeWithoutLastModified} T - A tree node type that extends the base TreeNode interface
451
+ * @param {string} version - The Unicode version string
452
+ * @param {T[]} entries - Array of file tree nodes from the API
453
+ * @returns {T[]} A new tree with normalized paths suitable for filtering
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * const apiTree = [{ type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt" }];
458
+ * const normalizedTree = normalizeTreeForFiltering("16.0.0", apiTree);
459
+ * // Returns: [{ type: "file", name: "Blocks.txt", path: "Blocks.txt" }]
460
+ * ```
461
+ */
462
+ function normalizeTreeForFiltering(version, entries) {
463
+ return entries.map((entry) => {
464
+ const normalizedPath = normalizePathForFiltering(version, entry.path);
465
+ if (entry.type === "directory" && entry.children) return {
466
+ ...entry,
467
+ path: normalizedPath,
468
+ children: normalizeTreeForFiltering(version, entry.children)
469
+ };
470
+ return {
471
+ ...entry,
472
+ path: normalizedPath
473
+ };
474
+ });
475
+ }
476
+ /**
477
+ * Recursively find a node (file or directory) by its path in the tree.
478
+ *
479
+ * @template T - A tree node type that extends the base TreeNode interface
480
+ * @param {T[]} entries - Array of file tree nodes that may contain nested children
481
+ * @param {string} targetPath - The path to search for
482
+ * @returns {T | undefined} The found node or undefined
483
+ */
484
+ function findFileByPath(entries, targetPath) {
485
+ for (const fileOrDirectory of entries) {
486
+ if ((fileOrDirectory.path ?? fileOrDirectory.name) === targetPath) return fileOrDirectory;
487
+ if (fileOrDirectory.type === "directory" && fileOrDirectory.children) {
488
+ const found = findFileByPath(fileOrDirectory.children, targetPath);
489
+ if (found) return found;
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * Recursively flattens a hierarchical file structure into an array of file paths.
495
+ *
496
+ * @template T - A tree node type that extends the base TreeNode interface
497
+ * @param {T[]} entries - Array of file tree nodes that may contain nested children
498
+ * @param {string} [prefix] - Optional path prefix to prepend to each file path (default: "")
499
+ * @returns {string[]} Array of flattened file paths as strings
500
+ *
501
+ * @example
502
+ * ```typescript
503
+ * import { flattenFilePaths } from "@ucdjs-internal/shared";
504
+ *
505
+ * const files = [
506
+ * { type: "directory", name: "folder1", path: "/folder1", children: [{ type: "file", name: "file1.txt", path: "/folder1/file1.txt" }] },
507
+ * { type: "file", name: "file2.txt", path: "/file2.txt" }
508
+ * ];
509
+ * const paths = flattenFilePaths(files);
510
+ * // Returns: ["/folder1/file1.txt", "/file2.txt"]
511
+ * ```
512
+ */
513
+ function flattenFilePaths(entries, prefix = "") {
514
+ const paths = [];
515
+ if (!Array.isArray(entries)) throw new TypeError("Expected 'entries' to be an array of file tree nodes.");
516
+ for (const file of entries) {
517
+ const fullPath = prefix ? `${prefix}${prependLeadingSlash(file.path)}` : file.path;
518
+ if (file.type === "directory" && file.children) paths.push(...flattenFilePaths(file.children, prefix));
519
+ else paths.push(fullPath);
520
+ }
521
+ return paths;
522
+ }
523
+
524
+ //#endregion
525
+ //#region src/glob.ts
526
+ /**
527
+ * Default picomatch options used across the shared package.
528
+ * These options ensure consistent glob matching behavior:
529
+ * - `nocase: true` - Case-insensitive matching
530
+ * - `dot: true` - Match dotfiles (files starting with `.`)
531
+ */
532
+ const DEFAULT_PICOMATCH_OPTIONS = {
533
+ nocase: true,
534
+ dot: true
535
+ };
536
+ function matchGlob(pattern, value, options = {}) {
537
+ const { nocase = DEFAULT_PICOMATCH_OPTIONS.nocase, dot = DEFAULT_PICOMATCH_OPTIONS.dot } = options;
538
+ return picomatch.isMatch(value, pattern, {
539
+ nocase,
540
+ dot
541
+ });
542
+ }
543
+ function createGlobMatcher(pattern, options = {}) {
544
+ const { nocase = DEFAULT_PICOMATCH_OPTIONS.nocase, dot = DEFAULT_PICOMATCH_OPTIONS.dot } = options;
545
+ const isMatch = picomatch(pattern, {
546
+ nocase,
547
+ dot
548
+ });
549
+ return (value) => isMatch(value);
550
+ }
551
+ const MAX_GLOB_LENGTH = 256;
552
+ const MAX_GLOB_SEGMENTS = 16;
553
+ const MAX_GLOB_BRACE_EXPANSIONS = 24;
554
+ const MAX_GLOB_STARS = 32;
555
+ const MAX_GLOB_QUESTIONS = 32;
556
+ function countWildcards(pattern) {
557
+ let stars = 0;
558
+ let questions = 0;
559
+ for (let i = 0; i < pattern.length; i += 1) {
560
+ const ch = pattern.charAt(i);
561
+ if (ch === "\\") {
562
+ i += 1;
563
+ continue;
564
+ }
565
+ if (ch === "*") stars += 1;
566
+ if (ch === "?") questions += 1;
567
+ }
568
+ return {
569
+ stars,
570
+ questions
571
+ };
572
+ }
573
+ function analyzeBraces(pattern) {
574
+ let braceDepth = 0;
575
+ const topLevelGroups = [];
576
+ const topLevelOptions = [];
577
+ let currentNestedStack = [];
578
+ for (let i = 0; i < pattern.length; i += 1) {
579
+ const ch = pattern.charAt(i);
580
+ if (ch === "\\") {
581
+ i += 1;
582
+ continue;
583
+ }
584
+ if (ch === "{") {
585
+ braceDepth += 1;
586
+ if (braceDepth === 1) {
587
+ topLevelOptions.push({
588
+ base: 1,
589
+ nestedMultiplier: 1
590
+ });
591
+ currentNestedStack = [];
592
+ } else currentNestedStack.push(1);
593
+ } else if (ch === "}") {
594
+ if (braceDepth === 0) return {
595
+ expansions: 0,
596
+ valid: false
597
+ };
598
+ braceDepth -= 1;
599
+ if (braceDepth === 0) {
600
+ let totalExpansions = 0;
601
+ for (const option of topLevelOptions) totalExpansions += option.base * option.nestedMultiplier;
602
+ topLevelGroups.push(totalExpansions);
603
+ topLevelOptions.length = 0;
604
+ currentNestedStack = [];
605
+ } else if (currentNestedStack.length > 0) {
606
+ const nestedCount = currentNestedStack.pop();
607
+ if (currentNestedStack.length > 0) currentNestedStack[currentNestedStack.length - 1] *= nestedCount;
608
+ else if (topLevelOptions.length > 0) topLevelOptions[topLevelOptions.length - 1].nestedMultiplier *= nestedCount;
609
+ }
610
+ } else if (ch === ",") {
611
+ if (braceDepth === 1) topLevelOptions.push({
612
+ base: 1,
613
+ nestedMultiplier: 1
614
+ });
615
+ else if (braceDepth > 1) {
616
+ if (currentNestedStack.length > 0) currentNestedStack[currentNestedStack.length - 1] += 1;
617
+ }
618
+ }
619
+ }
620
+ if (braceDepth !== 0) return {
621
+ expansions: 0,
622
+ valid: false
623
+ };
624
+ if (topLevelGroups.length === 0) return {
625
+ expansions: 0,
626
+ valid: true
627
+ };
628
+ return {
629
+ expansions: topLevelGroups.reduce((product, count) => product * count, 1),
630
+ valid: true
631
+ };
632
+ }
633
+ function isValidGlobPattern(pattern, limits = {}) {
634
+ const { maxLength = MAX_GLOB_LENGTH, maxSegments = MAX_GLOB_SEGMENTS, maxBraceExpansions = MAX_GLOB_BRACE_EXPANSIONS, maxStars = MAX_GLOB_STARS, maxQuestions = MAX_GLOB_QUESTIONS } = limits;
635
+ if (typeof pattern !== "string") return false;
636
+ if (pattern.length === 0) return false;
637
+ if (pattern.trim().length === 0) return false;
638
+ if (pattern.length > maxLength) return false;
639
+ if (pattern.includes("\0")) return false;
640
+ if (pattern.split(/[/\\]+/).filter(Boolean).length > maxSegments) return false;
641
+ const { stars, questions } = countWildcards(pattern);
642
+ if (stars > maxStars) return false;
643
+ if (questions > maxQuestions) return false;
644
+ const { expansions, valid: braceValid } = analyzeBraces(pattern);
645
+ if (!braceValid) return false;
646
+ if (expansions > maxBraceExpansions) return false;
647
+ try {
648
+ picomatch.scan(pattern);
649
+ return true;
650
+ } catch {
651
+ return false;
652
+ }
653
+ }
654
+
655
+ //#endregion
656
+ //#region src/filter.ts
657
+ /**
658
+ * File extensions excluded by default in createPathFilter.
659
+ * These are archive and document formats that are typically not needed for Unicode data processing.
660
+ */
661
+ const DEFAULT_EXCLUDED_EXTENSIONS = [
662
+ ".zip",
663
+ ".tar",
664
+ ".gz",
665
+ ".bz2",
666
+ ".xz",
667
+ ".7z",
668
+ ".rar",
669
+ ".pdf"
670
+ ];
671
+ /**
672
+ * Predefined filter patterns for common file exclusions.
673
+ * These constants can be used with `createPathFilter` to easily exclude common file types.
674
+ *
675
+ * @example
676
+ * ```ts
677
+ * import { createPathFilter, PRECONFIGURED_FILTERS } from '@ucdjs-internal/shared';
678
+ *
679
+ * const filter = createPathFilter({
680
+ * include: ['*.txt'],
681
+ * exclude: [
682
+ * ...PRECONFIGURED_FILTERS.TEST_FILES,
683
+ * ...PRECONFIGURED_FILTERS.README_FILES
684
+ * ]
685
+ * });
686
+ * ```
687
+ */
688
+ const PRECONFIGURED_FILTERS = {
689
+ TEST_FILES: ["**/*Test*"],
690
+ README_FILES: ["**/ReadMe.txt"],
691
+ HTML_FILES: ["**/*.html"]
692
+ };
693
+ /**
694
+ * Creates a filter function that checks if a file path should be included or excluded
695
+ * based on the provided filter configuration.
696
+ *
697
+ * @param {PathFilterOptions} options - Configuration object with include/exclude patterns
698
+ * @returns {PathFilter} A function that takes a path and returns true if the path should be included, false otherwise
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * import { createPathFilter, PRECONFIGURED_FILTERS } from '@ucdjs-internal/shared';
703
+ *
704
+ * // Include specific files, exclude others
705
+ * const filter = createPathFilter({
706
+ * include: ['src/**\/*.{js,ts}', 'test/**\/*.{test.js}'],
707
+ * exclude: ['**\/node_modules/**', '**\/*.generated.*']
708
+ * });
709
+ *
710
+ * // If include is empty/not set, includes everything
711
+ * const excludeOnly = createPathFilter({
712
+ * exclude: ['**\/node_modules/**', '**\/dist/**']
713
+ * });
714
+ *
715
+ * // Using preconfigured filters
716
+ * const withPresets = createPathFilter({
717
+ * include: ['src/**\/*.txt'],
718
+ * exclude: [
719
+ * ...PRECONFIGURED_FILTERS.TEST_FILES,
720
+ * ]
721
+ * });
722
+ * ```
723
+ */
724
+ function createPathFilter(options = {}) {
725
+ let currentConfig = { ...options };
726
+ let currentFilterFn = internal__createFilterFunction(currentConfig);
727
+ function filterFn(path, extraOptions = {}) {
728
+ if (!extraOptions.include && !extraOptions.exclude) return currentFilterFn(path);
729
+ return internal__createFilterFunction({
730
+ include: Array.from(new Set([...currentConfig.include || [], ...extraOptions.include || []])),
731
+ exclude: Array.from(new Set([...currentConfig.exclude || [], ...extraOptions.exclude || []])),
732
+ disableDefaultExclusions: currentConfig.disableDefaultExclusions
733
+ })(path);
734
+ }
735
+ filterFn.extend = (additionalOptions) => {
736
+ currentConfig = {
737
+ ...currentConfig,
738
+ include: [...currentConfig.include || [], ...additionalOptions.include || []],
739
+ exclude: [...currentConfig.exclude || [], ...additionalOptions.exclude || []]
740
+ };
741
+ currentFilterFn = internal__createFilterFunction(currentConfig);
742
+ };
743
+ filterFn.patterns = () => {
744
+ return Object.freeze(structuredClone(currentConfig));
745
+ };
746
+ return filterFn;
747
+ }
748
+ function normalizeForMatching(value) {
749
+ let normalized = value.replace(/\\/g, "/");
750
+ normalized = normalized.replace(/^\.\/+/, "");
751
+ normalized = normalized.replace(/^\//, "");
752
+ normalized = normalized.replace(/\/$/, "");
753
+ return normalized;
754
+ }
755
+ function normalizePatterns(patterns) {
756
+ const normalized = [];
757
+ for (let i = 0; i < patterns.length; i += 1) normalized.push(normalizeForMatching(patterns[i]));
758
+ return normalized;
759
+ }
760
+ function internal__createFilterFunction(config) {
761
+ const includePatterns = config.include && config.include.length > 0 ? config.include : ["**"];
762
+ const rawExcludePatterns = config.disableDefaultExclusions ? [...config.exclude || []] : [...DEFAULT_EXCLUDED_EXTENSIONS.map((ext) => `**/*${ext}`), ...config.exclude || []];
763
+ const normalizedIncludePatterns = normalizePatterns(includePatterns);
764
+ const excludePatterns = expandDirectoryPatterns(normalizePatterns(rawExcludePatterns));
765
+ return (path) => {
766
+ const normalizedPath = normalizeForMatching(path);
767
+ return picomatch.isMatch(normalizedPath, normalizedIncludePatterns, {
768
+ ...DEFAULT_PICOMATCH_OPTIONS,
769
+ ignore: excludePatterns
770
+ });
771
+ };
772
+ }
773
+ function expandDirectoryPatterns(patterns) {
774
+ const expanded = [];
775
+ for (const pattern of patterns) {
776
+ expanded.push(pattern);
777
+ if (isDirectoryOnlyPattern(pattern)) expanded.push(`${pattern}/**`);
778
+ }
779
+ return expanded;
780
+ }
781
+ function isDirectoryOnlyPattern(pattern) {
782
+ return !pattern.endsWith("/**") && !pattern.endsWith("/*") && !pattern.endsWith("/") && !pattern.includes(".") && !pattern.includes("*.") && (pattern.includes("/") || !pattern.includes("*"));
783
+ }
784
+ function filterTreeStructure(pathFilter, entries, extraOptions = {}) {
785
+ return internal__filterTreeStructure(pathFilter, entries, "", extraOptions);
786
+ }
787
+ function internal__filterTreeStructure(pathFilter, entries, parentPath, extraOptions) {
788
+ const filteredEntries = [];
789
+ for (const entry of entries) {
790
+ const fullPath = entry.path;
791
+ if (entry.type === "file") {
792
+ if (pathFilter(fullPath, extraOptions)) filteredEntries.push(entry);
793
+ } else if (entry.type === "directory") {
794
+ const filteredChildren = internal__filterTreeStructure(pathFilter, entry.children, fullPath, extraOptions);
795
+ const directoryMatches = pathFilter(fullPath, extraOptions);
796
+ const hasMatchingChildren = filteredChildren.length > 0;
797
+ if (directoryMatches || hasMatchingChildren) filteredEntries.push({
798
+ ...entry,
799
+ children: filteredChildren
800
+ });
801
+ }
802
+ }
803
+ return filteredEntries;
804
+ }
805
+
806
+ //#endregion
807
+ //#region src/guards.ts
808
+ /**
809
+ * Type guard function that checks if an unknown value is an ApiError object.
810
+ *
811
+ * This function performs runtime type checking to determine if the provided value
812
+ * conforms to the ApiError interface structure by verifying the presence of
813
+ * required properties: message, status, and timestamp.
814
+ *
815
+ * @param {unknown} error - The unknown value to check against the ApiError type
816
+ * @returns {error is ApiError} True if the value is an ApiError, false otherwise
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * import { isApiError } from "@ucdjs-internal/shared";
821
+ * import { client } from "@ucdjs/client";
822
+ *
823
+ * const { error, data } = await client.GET("/api/v1/versions");
824
+ * if (isApiError(error)) {
825
+ * console.error("API Error:", error.message);
826
+ * }
827
+ * ```
828
+ */
829
+ function isApiError(error) {
830
+ return typeof error === "object" && error !== null && "message" in error && "status" in error && "timestamp" in error && typeof error.message === "string" && typeof error.status === "number" && typeof error.timestamp === "string";
831
+ }
832
+
833
+ //#endregion
834
+ //#region src/ucd-config.ts
835
+ /**
836
+ * Fetches and validates the UCD well-known configuration from a server
837
+ *
838
+ * @param {string} baseUrl - The base URL of the UCD server
839
+ * @returns {Promise<UCDWellKnownConfig>} The validated well-known configuration
840
+ * @throws {Error} If the config cannot be fetched or is invalid
841
+ */
842
+ async function discoverEndpointsFromConfig(baseUrl) {
843
+ const url = new URL("/.well-known/ucd-config.json", baseUrl);
844
+ const fetchResult = await customFetch.safe(url.toString(), { parseAs: "json" });
845
+ if (fetchResult.error) throw fetchResult.error;
846
+ const result = UCDWellKnownConfigSchema.safeParse(fetchResult.data);
847
+ if (!result.success) throw new Error(`Invalid well-known config: ${result.error.message}`);
848
+ return result.data;
849
+ }
850
+ /**
851
+ * Return the default UCD well-known configuration used by the library.
852
+ *
853
+ * This function returns the build-time injected __UCD_ENDPOINT_DEFAULT_CONFIG__ if present;
854
+ * otherwise it falls back to a hard-coded default object containing a version and
855
+ * the common API endpoint paths for files, manifest and versions.
856
+ *
857
+ * The returned value conforms to the UCDWellKnownConfig schema and is used when
858
+ * discovery via discoverEndpointsFromConfig() is not possible or a local default
859
+ * is required.
860
+ *
861
+ * @returns {UCDWellKnownConfig} The default well-known configuration.
862
+ */
863
+ function getDefaultUCDEndpointConfig() {
864
+ return {
865
+ "version": "0.1",
866
+ "endpoints": {
867
+ "files": "/api/v1/files",
868
+ "manifest": "/.well-known/ucd-store/{version}.json",
869
+ "versions": "/api/v1/versions"
870
+ },
871
+ "versions": [
872
+ "17.0.0",
873
+ "16.0.0",
874
+ "15.1.0",
875
+ "15.0.0",
876
+ "14.0.0",
877
+ "13.0.0",
878
+ "12.1.0",
879
+ "12.0.0",
880
+ "11.0.0",
881
+ "10.0.0",
882
+ "9.0.0",
883
+ "8.0.0",
884
+ "7.0.0",
885
+ "6.3.0",
886
+ "6.2.0",
887
+ "6.1.0",
888
+ "6.0.0",
889
+ "5.2.0",
890
+ "5.1.0",
891
+ "5.0.0",
892
+ "4.1.0",
893
+ "4.0.1",
894
+ "4.0.0",
895
+ "3.2.0",
896
+ "3.1.1",
897
+ "3.1.0",
898
+ "3.0.1",
899
+ "3.0.0",
900
+ "2.1.9",
901
+ "2.1.8",
902
+ "2.1.5",
903
+ "2.1.2",
904
+ "2.0.0",
905
+ "1.1.5",
906
+ "1.1.0",
907
+ "1.0.1",
908
+ "1.0.0"
909
+ ]
910
+ };
911
+ }
912
+
913
+ //#endregion
914
+ export { DEFAULT_EXCLUDED_EXTENSIONS, DEFAULT_PICOMATCH_OPTIONS, PRECONFIGURED_FILTERS, createConcurrencyLimiter, createDebugger, createGlobMatcher, createPathFilter, customFetch, discoverEndpointsFromConfig, ensureIsPositiveConcurrency, filterTreeStructure, findFileByPath, flattenFilePaths, getDefaultUCDEndpointConfig, isApiError, isValidGlobPattern, matchGlob, normalizePathForFiltering, normalizeTreeForFiltering, safeJsonParse, tryOr, wrapTry };