@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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.mts +403 -0
- package/dist/index.mjs +914 -0
- package/package.json +64 -0
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 };
|