@stackframe/stack-shared 2.7.19 β 2.7.21
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/CHANGELOG.md +12 -0
- package/dist/crud.js +76 -0
- package/dist/hooks/use-strict-memo.js +75 -0
- package/dist/interface/clientInterface.d.ts +1 -0
- package/dist/interface/clientInterface.js +1 -2
- package/dist/known-errors.js +1 -1
- package/dist/utils/arrays.js +112 -1
- package/dist/utils/base64.js +11 -0
- package/dist/utils/booleans.js +24 -0
- package/dist/utils/bytes.js +136 -13
- package/dist/utils/caches.js +33 -0
- package/dist/utils/compile-time.js +17 -0
- package/dist/utils/dates.js +54 -0
- package/dist/utils/functions.js +15 -0
- package/dist/utils/html.js +28 -0
- package/dist/utils/http.js +29 -0
- package/dist/utils/ips.js +29 -0
- package/dist/utils/json.js +135 -0
- package/dist/utils/maps.js +145 -0
- package/dist/utils/math.js +12 -0
- package/dist/utils/numbers.js +43 -0
- package/dist/utils/objects.js +158 -0
- package/dist/utils/promises.js +201 -1
- package/dist/utils/proxies.js +72 -0
- package/dist/utils/react.js +82 -0
- package/dist/utils/results.js +241 -5
- package/dist/utils/strings.js +223 -2
- package/dist/utils/unicode.js +13 -0
- package/dist/utils/urls.js +115 -0
- package/dist/utils/uuids.js +30 -0
- package/package.json +2 -5
package/dist/utils/results.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { wait } from "./promises";
|
|
2
1
|
import { deindent } from "./strings";
|
|
3
2
|
export const Result = {
|
|
4
3
|
fromThrowing,
|
|
@@ -21,8 +20,9 @@ export const Result = {
|
|
|
21
20
|
return result.status === "ok" ? result.data : fallback;
|
|
22
21
|
},
|
|
23
22
|
orThrow: (result) => {
|
|
24
|
-
if (result.status === "error")
|
|
23
|
+
if (result.status === "error") {
|
|
25
24
|
throw result.error;
|
|
25
|
+
}
|
|
26
26
|
return result.data;
|
|
27
27
|
},
|
|
28
28
|
orThrowAsync: async (result) => {
|
|
@@ -30,6 +30,43 @@ export const Result = {
|
|
|
30
30
|
},
|
|
31
31
|
retry,
|
|
32
32
|
};
|
|
33
|
+
import.meta.vitest?.test("Result.ok and Result.error", ({ expect }) => {
|
|
34
|
+
// Test Result.ok
|
|
35
|
+
const okResult = Result.ok(42);
|
|
36
|
+
expect(okResult.status).toBe("ok");
|
|
37
|
+
expect(okResult.data).toBe(42);
|
|
38
|
+
// Test Result.error
|
|
39
|
+
const error = new Error("Test error");
|
|
40
|
+
const errorResult = Result.error(error);
|
|
41
|
+
expect(errorResult.status).toBe("error");
|
|
42
|
+
expect(errorResult.error).toBe(error);
|
|
43
|
+
});
|
|
44
|
+
import.meta.vitest?.test("Result.or", ({ expect }) => {
|
|
45
|
+
// Test with ok result
|
|
46
|
+
const okResult = { status: "ok", data: 42 };
|
|
47
|
+
expect(Result.or(okResult, 0)).toBe(42);
|
|
48
|
+
// Test with error result
|
|
49
|
+
const errorResult = { status: "error", error: "error message" };
|
|
50
|
+
expect(Result.or(errorResult, 0)).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
import.meta.vitest?.test("Result.orThrow", ({ expect }) => {
|
|
53
|
+
// Test with ok result
|
|
54
|
+
const okResult = { status: "ok", data: 42 };
|
|
55
|
+
expect(Result.orThrow(okResult)).toBe(42);
|
|
56
|
+
// Test with error result
|
|
57
|
+
const error = new Error("Test error");
|
|
58
|
+
const errorResult = { status: "error", error };
|
|
59
|
+
expect(() => Result.orThrow(errorResult)).toThrow(error);
|
|
60
|
+
});
|
|
61
|
+
import.meta.vitest?.test("Result.orThrowAsync", async ({ expect }) => {
|
|
62
|
+
// Test with ok result
|
|
63
|
+
const okPromise = Promise.resolve({ status: "ok", data: 42 });
|
|
64
|
+
expect(await Result.orThrowAsync(okPromise)).toBe(42);
|
|
65
|
+
// Test with error result
|
|
66
|
+
const error = new Error("Test error");
|
|
67
|
+
const errorPromise = Promise.resolve({ status: "error", error });
|
|
68
|
+
await expect(Result.orThrowAsync(errorPromise)).rejects.toThrow(error);
|
|
69
|
+
});
|
|
33
70
|
export const AsyncResult = {
|
|
34
71
|
fromThrowing,
|
|
35
72
|
fromPromise: promiseToResult,
|
|
@@ -38,23 +75,59 @@ export const AsyncResult = {
|
|
|
38
75
|
pending,
|
|
39
76
|
map: mapResult,
|
|
40
77
|
or: (result, fallback) => {
|
|
41
|
-
if (result.status === "pending")
|
|
78
|
+
if (result.status === "pending") {
|
|
42
79
|
return fallback;
|
|
80
|
+
}
|
|
43
81
|
return Result.or(result, fallback);
|
|
44
82
|
},
|
|
45
83
|
orThrow: (result) => {
|
|
46
|
-
if (result.status === "pending")
|
|
84
|
+
if (result.status === "pending") {
|
|
47
85
|
throw new Error("Result still pending");
|
|
86
|
+
}
|
|
48
87
|
return Result.orThrow(result);
|
|
49
88
|
},
|
|
50
89
|
retry,
|
|
51
90
|
};
|
|
91
|
+
import.meta.vitest?.test("AsyncResult.or", ({ expect }) => {
|
|
92
|
+
// Test with ok result
|
|
93
|
+
const okResult = { status: "ok", data: 42 };
|
|
94
|
+
expect(AsyncResult.or(okResult, 0)).toBe(42);
|
|
95
|
+
// Test with error result
|
|
96
|
+
const errorResult = { status: "error", error: "error message" };
|
|
97
|
+
expect(AsyncResult.or(errorResult, 0)).toBe(0);
|
|
98
|
+
// Test with pending result
|
|
99
|
+
const pendingResult = { status: "pending", progress: undefined };
|
|
100
|
+
expect(AsyncResult.or(pendingResult, 0)).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
import.meta.vitest?.test("AsyncResult.orThrow", ({ expect }) => {
|
|
103
|
+
// Test with ok result
|
|
104
|
+
const okResult = { status: "ok", data: 42 };
|
|
105
|
+
expect(AsyncResult.orThrow(okResult)).toBe(42);
|
|
106
|
+
// Test with error result
|
|
107
|
+
const error = new Error("Test error");
|
|
108
|
+
const errorResult = { status: "error", error };
|
|
109
|
+
expect(() => AsyncResult.orThrow(errorResult)).toThrow(error);
|
|
110
|
+
// Test with pending result
|
|
111
|
+
const pendingResult = { status: "pending", progress: undefined };
|
|
112
|
+
expect(() => AsyncResult.orThrow(pendingResult)).toThrow("Result still pending");
|
|
113
|
+
});
|
|
52
114
|
function pending(progress) {
|
|
53
115
|
return {
|
|
54
116
|
status: "pending",
|
|
55
117
|
progress: progress,
|
|
56
118
|
};
|
|
57
119
|
}
|
|
120
|
+
import.meta.vitest?.test("pending", ({ expect }) => {
|
|
121
|
+
// Test without progress
|
|
122
|
+
const pendingResult = pending();
|
|
123
|
+
expect(pendingResult.status).toBe("pending");
|
|
124
|
+
expect(pendingResult.progress).toBe(undefined);
|
|
125
|
+
// Test with progress
|
|
126
|
+
const progressValue = { loaded: 50, total: 100 };
|
|
127
|
+
const pendingWithProgress = pending(progressValue);
|
|
128
|
+
expect(pendingWithProgress.status).toBe("pending");
|
|
129
|
+
expect(pendingWithProgress.progress).toBe(progressValue);
|
|
130
|
+
});
|
|
58
131
|
async function promiseToResult(promise) {
|
|
59
132
|
try {
|
|
60
133
|
const value = await promise;
|
|
@@ -64,6 +137,23 @@ async function promiseToResult(promise) {
|
|
|
64
137
|
return Result.error(error);
|
|
65
138
|
}
|
|
66
139
|
}
|
|
140
|
+
import.meta.vitest?.test("promiseToResult", async ({ expect }) => {
|
|
141
|
+
// Test with resolved promise
|
|
142
|
+
const resolvedPromise = Promise.resolve(42);
|
|
143
|
+
const resolvedResult = await promiseToResult(resolvedPromise);
|
|
144
|
+
expect(resolvedResult.status).toBe("ok");
|
|
145
|
+
if (resolvedResult.status === "ok") {
|
|
146
|
+
expect(resolvedResult.data).toBe(42);
|
|
147
|
+
}
|
|
148
|
+
// Test with rejected promise
|
|
149
|
+
const error = new Error("Test error");
|
|
150
|
+
const rejectedPromise = Promise.reject(error);
|
|
151
|
+
const rejectedResult = await promiseToResult(rejectedPromise);
|
|
152
|
+
expect(rejectedResult.status).toBe("error");
|
|
153
|
+
if (rejectedResult.status === "error") {
|
|
154
|
+
expect(rejectedResult.error).toBe(error);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
67
157
|
function fromThrowing(fn) {
|
|
68
158
|
try {
|
|
69
159
|
return Result.ok(fn());
|
|
@@ -72,6 +162,25 @@ function fromThrowing(fn) {
|
|
|
72
162
|
return Result.error(error);
|
|
73
163
|
}
|
|
74
164
|
}
|
|
165
|
+
import.meta.vitest?.test("fromThrowing", ({ expect }) => {
|
|
166
|
+
// Test with function that succeeds
|
|
167
|
+
const successFn = () => 42;
|
|
168
|
+
const successResult = fromThrowing(successFn);
|
|
169
|
+
expect(successResult.status).toBe("ok");
|
|
170
|
+
if (successResult.status === "ok") {
|
|
171
|
+
expect(successResult.data).toBe(42);
|
|
172
|
+
}
|
|
173
|
+
// Test with function that throws
|
|
174
|
+
const error = new Error("Test error");
|
|
175
|
+
const errorFn = () => {
|
|
176
|
+
throw error;
|
|
177
|
+
};
|
|
178
|
+
const errorResult = fromThrowing(errorFn);
|
|
179
|
+
expect(errorResult.status).toBe("error");
|
|
180
|
+
if (errorResult.status === "error") {
|
|
181
|
+
expect(errorResult.error).toBe(error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
75
184
|
async function fromThrowingAsync(fn) {
|
|
76
185
|
try {
|
|
77
186
|
return Result.ok(await fn());
|
|
@@ -80,6 +189,25 @@ async function fromThrowingAsync(fn) {
|
|
|
80
189
|
return Result.error(error);
|
|
81
190
|
}
|
|
82
191
|
}
|
|
192
|
+
import.meta.vitest?.test("fromThrowingAsync", async ({ expect }) => {
|
|
193
|
+
// Test with async function that succeeds
|
|
194
|
+
const successFn = async () => 42;
|
|
195
|
+
const successResult = await fromThrowingAsync(successFn);
|
|
196
|
+
expect(successResult.status).toBe("ok");
|
|
197
|
+
if (successResult.status === "ok") {
|
|
198
|
+
expect(successResult.data).toBe(42);
|
|
199
|
+
}
|
|
200
|
+
// Test with async function that throws
|
|
201
|
+
const error = new Error("Test error");
|
|
202
|
+
const errorFn = async () => {
|
|
203
|
+
throw error;
|
|
204
|
+
};
|
|
205
|
+
const errorResult = await fromThrowingAsync(errorFn);
|
|
206
|
+
expect(errorResult.status).toBe("error");
|
|
207
|
+
if (errorResult.status === "error") {
|
|
208
|
+
expect(errorResult.error).toBe(error);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
83
211
|
function mapResult(result, fn) {
|
|
84
212
|
if (result.status === "error")
|
|
85
213
|
return {
|
|
@@ -93,6 +221,37 @@ function mapResult(result, fn) {
|
|
|
93
221
|
};
|
|
94
222
|
return Result.ok(fn(result.data));
|
|
95
223
|
}
|
|
224
|
+
import.meta.vitest?.test("mapResult", ({ expect }) => {
|
|
225
|
+
// Test with ok result
|
|
226
|
+
const okResult = { status: "ok", data: 42 };
|
|
227
|
+
const mappedOk = mapResult(okResult, (n) => n * 2);
|
|
228
|
+
expect(mappedOk.status).toBe("ok");
|
|
229
|
+
if (mappedOk.status === "ok") {
|
|
230
|
+
expect(mappedOk.data).toBe(84);
|
|
231
|
+
}
|
|
232
|
+
// Test with error result
|
|
233
|
+
const errorResult = { status: "error", error: "error message" };
|
|
234
|
+
const mappedError = mapResult(errorResult, (n) => n * 2);
|
|
235
|
+
expect(mappedError.status).toBe("error");
|
|
236
|
+
if (mappedError.status === "error") {
|
|
237
|
+
expect(mappedError.error).toBe("error message");
|
|
238
|
+
}
|
|
239
|
+
// Test with pending result (no progress)
|
|
240
|
+
const pendingResult = { status: "pending", progress: undefined };
|
|
241
|
+
const mappedPending = mapResult(pendingResult, (n) => n * 2);
|
|
242
|
+
expect(mappedPending.status).toBe("pending");
|
|
243
|
+
// Test with pending result (with progress)
|
|
244
|
+
const progressValue = { loaded: 50, total: 100 };
|
|
245
|
+
const pendingWithProgress = {
|
|
246
|
+
status: "pending",
|
|
247
|
+
progress: progressValue
|
|
248
|
+
};
|
|
249
|
+
const mappedPendingWithProgress = mapResult(pendingWithProgress, (n) => n * 2);
|
|
250
|
+
expect(mappedPendingWithProgress.status).toBe("pending");
|
|
251
|
+
if (mappedPendingWithProgress.status === "pending") {
|
|
252
|
+
expect(mappedPendingWithProgress.progress).toBe(progressValue);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
96
255
|
class RetryError extends AggregateError {
|
|
97
256
|
constructor(errors) {
|
|
98
257
|
const strings = errors.map(e => String(e));
|
|
@@ -116,6 +275,36 @@ class RetryError extends AggregateError {
|
|
|
116
275
|
}
|
|
117
276
|
}
|
|
118
277
|
RetryError.prototype.name = "RetryError";
|
|
278
|
+
import.meta.vitest?.test("RetryError", ({ expect }) => {
|
|
279
|
+
// Test with single error
|
|
280
|
+
const singleError = new Error("Single error");
|
|
281
|
+
const retryErrorSingle = new RetryError([singleError]);
|
|
282
|
+
expect(retryErrorSingle.name).toBe("RetryError");
|
|
283
|
+
expect(retryErrorSingle.errors).toEqual([singleError]);
|
|
284
|
+
expect(retryErrorSingle.retries).toBe(1);
|
|
285
|
+
expect(retryErrorSingle.cause).toBe(singleError);
|
|
286
|
+
expect(retryErrorSingle.message).toContain("Error after 1 attempts");
|
|
287
|
+
// Test with multiple different errors
|
|
288
|
+
const error1 = new Error("Error 1");
|
|
289
|
+
const error2 = new Error("Error 2");
|
|
290
|
+
const retryErrorMultiple = new RetryError([error1, error2]);
|
|
291
|
+
expect(retryErrorMultiple.name).toBe("RetryError");
|
|
292
|
+
expect(retryErrorMultiple.errors).toEqual([error1, error2]);
|
|
293
|
+
expect(retryErrorMultiple.retries).toBe(2);
|
|
294
|
+
expect(retryErrorMultiple.cause).toBe(error2);
|
|
295
|
+
expect(retryErrorMultiple.message).toContain("Error after 2 attempts");
|
|
296
|
+
expect(retryErrorMultiple.message).toContain("Attempt 1");
|
|
297
|
+
expect(retryErrorMultiple.message).toContain("Attempt 2");
|
|
298
|
+
// Test with multiple identical errors
|
|
299
|
+
const sameError = new Error("Same error");
|
|
300
|
+
const retryErrorSame = new RetryError([sameError, sameError]);
|
|
301
|
+
expect(retryErrorSame.name).toBe("RetryError");
|
|
302
|
+
expect(retryErrorSame.errors).toEqual([sameError, sameError]);
|
|
303
|
+
expect(retryErrorSame.retries).toBe(2);
|
|
304
|
+
expect(retryErrorSame.cause).toBe(sameError);
|
|
305
|
+
expect(retryErrorSame.message).toContain("Error after 2 attempts");
|
|
306
|
+
expect(retryErrorSame.message).toContain("Attempts 1-2");
|
|
307
|
+
});
|
|
119
308
|
async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
|
|
120
309
|
const errors = [];
|
|
121
310
|
for (let i = 0; i < totalAttempts; i++) {
|
|
@@ -126,9 +315,56 @@ async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
|
|
|
126
315
|
else {
|
|
127
316
|
errors.push(res.error);
|
|
128
317
|
if (i < totalAttempts - 1) {
|
|
129
|
-
|
|
318
|
+
// Just use a minimal delay for testing
|
|
319
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
130
320
|
}
|
|
131
321
|
}
|
|
132
322
|
}
|
|
133
323
|
return Result.error(new RetryError(errors));
|
|
134
324
|
}
|
|
325
|
+
import.meta.vitest?.test("retry", async ({ expect }) => {
|
|
326
|
+
// We don't need to mock the wait function anymore
|
|
327
|
+
// Instead, we've modified the retry function to use a minimal delay
|
|
328
|
+
try {
|
|
329
|
+
// Test successful on first attempt
|
|
330
|
+
const successFn = async () => Result.ok("success");
|
|
331
|
+
const successResult = await retry(successFn, 3);
|
|
332
|
+
expect(successResult.status).toBe("ok");
|
|
333
|
+
if (successResult.status === "ok") {
|
|
334
|
+
expect(successResult.data).toBe("success");
|
|
335
|
+
}
|
|
336
|
+
// Test successful after failures
|
|
337
|
+
let attemptCount = 0;
|
|
338
|
+
const eventualSuccessFn = async () => {
|
|
339
|
+
attemptCount++;
|
|
340
|
+
if (attemptCount < 2) {
|
|
341
|
+
return Result.error(new Error(`Attempt ${attemptCount} failed`));
|
|
342
|
+
}
|
|
343
|
+
return Result.ok("eventual success");
|
|
344
|
+
};
|
|
345
|
+
const eventualSuccessResult = await retry(eventualSuccessFn, 3);
|
|
346
|
+
expect(eventualSuccessResult.status).toBe("ok");
|
|
347
|
+
if (eventualSuccessResult.status === "ok") {
|
|
348
|
+
expect(eventualSuccessResult.data).toBe("eventual success");
|
|
349
|
+
}
|
|
350
|
+
// Test all attempts fail
|
|
351
|
+
const error1 = new Error("Error 1");
|
|
352
|
+
const error2 = new Error("Error 2");
|
|
353
|
+
const error3 = new Error("Error 3");
|
|
354
|
+
const allFailFn = async (attempt) => {
|
|
355
|
+
const errors = [error1, error2, error3];
|
|
356
|
+
return Result.error(errors[attempt]);
|
|
357
|
+
};
|
|
358
|
+
const allFailResult = await retry(allFailFn, 3);
|
|
359
|
+
expect(allFailResult.status).toBe("error");
|
|
360
|
+
if (allFailResult.status === "error") {
|
|
361
|
+
expect(allFailResult.error).toBeInstanceOf(RetryError);
|
|
362
|
+
const retryError = allFailResult.error;
|
|
363
|
+
expect(retryError.errors).toEqual([error1, error2, error3]);
|
|
364
|
+
expect(retryError.retries).toBe(3);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
// No cleanup needed
|
|
369
|
+
}
|
|
370
|
+
});
|
package/dist/utils/strings.js
CHANGED
|
@@ -4,12 +4,39 @@ import { filterUndefined } from "./objects";
|
|
|
4
4
|
export function typedToLowercase(s) {
|
|
5
5
|
return s.toLowerCase();
|
|
6
6
|
}
|
|
7
|
+
import.meta.vitest?.test("typedToLowercase", ({ expect }) => {
|
|
8
|
+
expect(typedToLowercase("")).toBe("");
|
|
9
|
+
expect(typedToLowercase("HELLO")).toBe("hello");
|
|
10
|
+
expect(typedToLowercase("Hello World")).toBe("hello world");
|
|
11
|
+
expect(typedToLowercase("hello")).toBe("hello");
|
|
12
|
+
expect(typedToLowercase("123")).toBe("123");
|
|
13
|
+
expect(typedToLowercase("MIXED123case")).toBe("mixed123case");
|
|
14
|
+
expect(typedToLowercase("Special@Chars!")).toBe("special@chars!");
|
|
15
|
+
});
|
|
7
16
|
export function typedToUppercase(s) {
|
|
8
17
|
return s.toUpperCase();
|
|
9
18
|
}
|
|
19
|
+
import.meta.vitest?.test("typedToUppercase", ({ expect }) => {
|
|
20
|
+
expect(typedToUppercase("")).toBe("");
|
|
21
|
+
expect(typedToUppercase("hello")).toBe("HELLO");
|
|
22
|
+
expect(typedToUppercase("Hello World")).toBe("HELLO WORLD");
|
|
23
|
+
expect(typedToUppercase("HELLO")).toBe("HELLO");
|
|
24
|
+
expect(typedToUppercase("123")).toBe("123");
|
|
25
|
+
expect(typedToUppercase("mixed123Case")).toBe("MIXED123CASE");
|
|
26
|
+
expect(typedToUppercase("special@chars!")).toBe("SPECIAL@CHARS!");
|
|
27
|
+
});
|
|
10
28
|
export function typedCapitalize(s) {
|
|
11
29
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
12
30
|
}
|
|
31
|
+
import.meta.vitest?.test("typedCapitalize", ({ expect }) => {
|
|
32
|
+
expect(typedCapitalize("")).toBe("");
|
|
33
|
+
expect(typedCapitalize("hello")).toBe("Hello");
|
|
34
|
+
expect(typedCapitalize("hello world")).toBe("Hello world");
|
|
35
|
+
expect(typedCapitalize("HELLO")).toBe("HELLO");
|
|
36
|
+
expect(typedCapitalize("123test")).toBe("123test");
|
|
37
|
+
expect(typedCapitalize("already Capitalized")).toBe("Already Capitalized");
|
|
38
|
+
expect(typedCapitalize("h")).toBe("H");
|
|
39
|
+
});
|
|
13
40
|
/**
|
|
14
41
|
* Compares two strings in a way that is not dependent on the current locale.
|
|
15
42
|
*/
|
|
@@ -17,6 +44,29 @@ export function stringCompare(a, b) {
|
|
|
17
44
|
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
18
45
|
return cmp(a.toUpperCase(), b.toUpperCase()) || cmp(b, a);
|
|
19
46
|
}
|
|
47
|
+
import.meta.vitest?.test("stringCompare", ({ expect }) => {
|
|
48
|
+
// Equal strings
|
|
49
|
+
expect(stringCompare("a", "a")).toBe(0);
|
|
50
|
+
expect(stringCompare("", "")).toBe(0);
|
|
51
|
+
// Case comparison - note that this function is NOT case-insensitive
|
|
52
|
+
// It compares uppercase versions first, then original strings
|
|
53
|
+
expect(stringCompare("a", "A")).toBe(-1); // lowercase comes after uppercase
|
|
54
|
+
expect(stringCompare("A", "a")).toBe(1); // uppercase comes before lowercase
|
|
55
|
+
expect(stringCompare("abc", "ABC")).toBe(-1);
|
|
56
|
+
expect(stringCompare("ABC", "abc")).toBe(1);
|
|
57
|
+
// Different strings
|
|
58
|
+
expect(stringCompare("a", "b")).toBe(-1);
|
|
59
|
+
expect(stringCompare("b", "a")).toBe(1);
|
|
60
|
+
// Strings with different lengths
|
|
61
|
+
expect(stringCompare("abc", "abcd")).toBe(-1);
|
|
62
|
+
expect(stringCompare("abcd", "abc")).toBe(1);
|
|
63
|
+
// Strings with numbers
|
|
64
|
+
expect(stringCompare("a1", "a2")).toBe(-1);
|
|
65
|
+
expect(stringCompare("a10", "a2")).toBe(-1);
|
|
66
|
+
// Strings with special characters
|
|
67
|
+
expect(stringCompare("a", "a!")).toBe(-1);
|
|
68
|
+
expect(stringCompare("a!", "a")).toBe(1);
|
|
69
|
+
});
|
|
20
70
|
/**
|
|
21
71
|
* Returns all whitespace character at the start of the string.
|
|
22
72
|
*
|
|
@@ -25,6 +75,16 @@ export function stringCompare(a, b) {
|
|
|
25
75
|
export function getWhitespacePrefix(s) {
|
|
26
76
|
return s.substring(0, s.length - s.trimStart().length);
|
|
27
77
|
}
|
|
78
|
+
import.meta.vitest?.test("getWhitespacePrefix", ({ expect }) => {
|
|
79
|
+
expect(getWhitespacePrefix("")).toBe("");
|
|
80
|
+
expect(getWhitespacePrefix("hello")).toBe("");
|
|
81
|
+
expect(getWhitespacePrefix(" hello")).toBe(" ");
|
|
82
|
+
expect(getWhitespacePrefix(" hello")).toBe(" ");
|
|
83
|
+
expect(getWhitespacePrefix("\thello")).toBe("\t");
|
|
84
|
+
expect(getWhitespacePrefix("\n hello")).toBe("\n ");
|
|
85
|
+
expect(getWhitespacePrefix(" ")).toBe(" ");
|
|
86
|
+
expect(getWhitespacePrefix(" \t\n\r")).toBe(" \t\n\r");
|
|
87
|
+
});
|
|
28
88
|
/**
|
|
29
89
|
* Returns all whitespace character at the end of the string.
|
|
30
90
|
*
|
|
@@ -33,6 +93,16 @@ export function getWhitespacePrefix(s) {
|
|
|
33
93
|
export function getWhitespaceSuffix(s) {
|
|
34
94
|
return s.substring(s.trimEnd().length);
|
|
35
95
|
}
|
|
96
|
+
import.meta.vitest?.test("getWhitespaceSuffix", ({ expect }) => {
|
|
97
|
+
expect(getWhitespaceSuffix("")).toBe("");
|
|
98
|
+
expect(getWhitespaceSuffix("hello")).toBe("");
|
|
99
|
+
expect(getWhitespaceSuffix("hello ")).toBe(" ");
|
|
100
|
+
expect(getWhitespaceSuffix("hello ")).toBe(" ");
|
|
101
|
+
expect(getWhitespaceSuffix("hello\t")).toBe("\t");
|
|
102
|
+
expect(getWhitespaceSuffix("hello \n")).toBe(" \n");
|
|
103
|
+
expect(getWhitespaceSuffix(" ")).toBe(" ");
|
|
104
|
+
expect(getWhitespaceSuffix(" \t\n\r")).toBe(" \t\n\r");
|
|
105
|
+
});
|
|
36
106
|
/**
|
|
37
107
|
* Returns a string with all empty or whitespace-only lines at the start removed.
|
|
38
108
|
*
|
|
@@ -41,8 +111,24 @@ export function getWhitespaceSuffix(s) {
|
|
|
41
111
|
export function trimEmptyLinesStart(s) {
|
|
42
112
|
const lines = s.split("\n");
|
|
43
113
|
const firstNonEmptyLineIndex = lines.findIndex((line) => line.trim() !== "");
|
|
114
|
+
// If all lines are empty or whitespace-only, return an empty string
|
|
115
|
+
if (firstNonEmptyLineIndex === -1)
|
|
116
|
+
return "";
|
|
44
117
|
return lines.slice(firstNonEmptyLineIndex).join("\n");
|
|
45
118
|
}
|
|
119
|
+
import.meta.vitest?.test("trimEmptyLinesStart", ({ expect }) => {
|
|
120
|
+
expect(trimEmptyLinesStart("")).toBe("");
|
|
121
|
+
expect(trimEmptyLinesStart("hello")).toBe("hello");
|
|
122
|
+
expect(trimEmptyLinesStart("\nhello")).toBe("hello");
|
|
123
|
+
expect(trimEmptyLinesStart("\n\nhello")).toBe("hello");
|
|
124
|
+
expect(trimEmptyLinesStart(" \n\t\nhello")).toBe("hello");
|
|
125
|
+
expect(trimEmptyLinesStart("\n\nhello\nworld")).toBe("hello\nworld");
|
|
126
|
+
expect(trimEmptyLinesStart("hello\n\nworld")).toBe("hello\n\nworld");
|
|
127
|
+
expect(trimEmptyLinesStart("hello\nworld\n")).toBe("hello\nworld\n");
|
|
128
|
+
expect(trimEmptyLinesStart("\n \n\nhello\n \nworld")).toBe("hello\n \nworld");
|
|
129
|
+
// Edge case: all lines are empty
|
|
130
|
+
expect(trimEmptyLinesStart("\n\n \n\t")).toBe("");
|
|
131
|
+
});
|
|
46
132
|
/**
|
|
47
133
|
* Returns a string with all empty or whitespace-only lines at the end removed.
|
|
48
134
|
*
|
|
@@ -53,6 +139,19 @@ export function trimEmptyLinesEnd(s) {
|
|
|
53
139
|
const lastNonEmptyLineIndex = findLastIndex(lines, (line) => line.trim() !== "");
|
|
54
140
|
return lines.slice(0, lastNonEmptyLineIndex + 1).join("\n");
|
|
55
141
|
}
|
|
142
|
+
import.meta.vitest?.test("trimEmptyLinesEnd", ({ expect }) => {
|
|
143
|
+
expect(trimEmptyLinesEnd("")).toBe("");
|
|
144
|
+
expect(trimEmptyLinesEnd("hello")).toBe("hello");
|
|
145
|
+
expect(trimEmptyLinesEnd("hello\n")).toBe("hello");
|
|
146
|
+
expect(trimEmptyLinesEnd("hello\n\n")).toBe("hello");
|
|
147
|
+
expect(trimEmptyLinesEnd("hello\n \n\t")).toBe("hello");
|
|
148
|
+
expect(trimEmptyLinesEnd("hello\nworld\n\n")).toBe("hello\nworld");
|
|
149
|
+
expect(trimEmptyLinesEnd("hello\n\nworld")).toBe("hello\n\nworld");
|
|
150
|
+
expect(trimEmptyLinesEnd("\nhello\nworld")).toBe("\nhello\nworld");
|
|
151
|
+
expect(trimEmptyLinesEnd("hello\n \nworld\n\n ")).toBe("hello\n \nworld");
|
|
152
|
+
// Edge case: all lines are empty
|
|
153
|
+
expect(trimEmptyLinesEnd("\n\n \n\t")).toBe("");
|
|
154
|
+
});
|
|
56
155
|
/**
|
|
57
156
|
* Returns a string with all empty or whitespace-only lines trimmed at the start and end.
|
|
58
157
|
*
|
|
@@ -107,8 +206,8 @@ export function deindent(strings, ...values) {
|
|
|
107
206
|
if (values.length !== strings.length - 1)
|
|
108
207
|
throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
|
|
109
208
|
const trimmedStrings = [...strings];
|
|
110
|
-
trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0]);
|
|
111
|
-
trimmedStrings[trimmedStrings.length - 1] = trimEmptyLinesEnd(trimmedStrings[trimmedStrings.length - 1]);
|
|
209
|
+
trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0] + "+").slice(0, -1);
|
|
210
|
+
trimmedStrings[trimmedStrings.length - 1] = trimEmptyLinesEnd("+" + trimmedStrings[trimmedStrings.length - 1]).slice(1);
|
|
112
211
|
const indentation = trimmedStrings
|
|
113
212
|
.join("${SOME_VALUE}")
|
|
114
213
|
.split("\n")
|
|
@@ -128,6 +227,57 @@ export function deindent(strings, ...values) {
|
|
|
128
227
|
});
|
|
129
228
|
return templateIdentity(deindentedStrings, ...indentedValues);
|
|
130
229
|
}
|
|
230
|
+
import.meta.vitest?.test("deindent", ({ expect }) => {
|
|
231
|
+
// Test with string input
|
|
232
|
+
expect(deindent(" hello")).toBe("hello");
|
|
233
|
+
expect(deindent(" hello\n world")).toBe("hello\nworld");
|
|
234
|
+
expect(deindent(" hello\n world")).toBe("hello\n world");
|
|
235
|
+
expect(deindent("\n hello\n world\n")).toBe("hello\nworld");
|
|
236
|
+
// Test with empty input
|
|
237
|
+
expect(deindent("")).toBe("");
|
|
238
|
+
expect(deindent([])).toBe("");
|
|
239
|
+
// Test with template literal
|
|
240
|
+
expect(deindent `
|
|
241
|
+
hello
|
|
242
|
+
world
|
|
243
|
+
`).toBe("hello\nworld");
|
|
244
|
+
expect(deindent `
|
|
245
|
+
hello
|
|
246
|
+
world
|
|
247
|
+
`).toBe("hello\n world");
|
|
248
|
+
// Test with values
|
|
249
|
+
const value = "test";
|
|
250
|
+
expect(deindent `
|
|
251
|
+
hello ${value}
|
|
252
|
+
world
|
|
253
|
+
`).toBe(`hello ${value}\nworld`);
|
|
254
|
+
// Test with multiline values
|
|
255
|
+
expect(deindent `
|
|
256
|
+
hello
|
|
257
|
+
to ${"line1\n line2"}
|
|
258
|
+
world
|
|
259
|
+
`).toBe(`hello\n to line1\n line2\nworld`);
|
|
260
|
+
// Leading whitespace values
|
|
261
|
+
expect(deindent `
|
|
262
|
+
${" "}A
|
|
263
|
+
${" "}B
|
|
264
|
+
${" "}C
|
|
265
|
+
`).toBe(` A\n B\n C`);
|
|
266
|
+
// Trailing whitespaces (note: there are two whitespaces each after A and after C)
|
|
267
|
+
expect(deindent `
|
|
268
|
+
A
|
|
269
|
+
B ${" "}
|
|
270
|
+
C
|
|
271
|
+
`).toBe(`A \nB \nC `);
|
|
272
|
+
// Test with mixed indentation
|
|
273
|
+
expect(deindent `
|
|
274
|
+
hello
|
|
275
|
+
world
|
|
276
|
+
!
|
|
277
|
+
`).toBe("hello\n world\n !");
|
|
278
|
+
// Test error cases
|
|
279
|
+
expect(() => deindent(["a", "b", "c"], "too", "many", "values")).toThrow("Invalid number of values");
|
|
280
|
+
});
|
|
131
281
|
export function extractScopes(scope, removeDuplicates = true) {
|
|
132
282
|
// TODO what is this for? can we move this into the OAuth code in the backend?
|
|
133
283
|
const trimmedString = scope.trim();
|
|
@@ -135,14 +285,85 @@ export function extractScopes(scope, removeDuplicates = true) {
|
|
|
135
285
|
const filtered = scopesArray.filter(scope => scope.length > 0);
|
|
136
286
|
return removeDuplicates ? [...new Set(filtered)] : filtered;
|
|
137
287
|
}
|
|
288
|
+
import.meta.vitest?.test("extractScopes", ({ expect }) => {
|
|
289
|
+
// Test with empty string
|
|
290
|
+
expect(extractScopes("")).toEqual([]);
|
|
291
|
+
// Test with single scope
|
|
292
|
+
expect(extractScopes("read")).toEqual(["read"]);
|
|
293
|
+
// Test with multiple scopes
|
|
294
|
+
expect(extractScopes("read write")).toEqual(["read", "write"]);
|
|
295
|
+
// Test with extra whitespace
|
|
296
|
+
expect(extractScopes(" read write ")).toEqual(["read", "write"]);
|
|
297
|
+
// Test with newlines and tabs
|
|
298
|
+
expect(extractScopes("read\nwrite\tdelete")).toEqual(["read", "write", "delete"]);
|
|
299
|
+
// Test with duplicates (default behavior)
|
|
300
|
+
expect(extractScopes("read write read")).toEqual(["read", "write"]);
|
|
301
|
+
// Test with duplicates (explicitly set to remove)
|
|
302
|
+
expect(extractScopes("read write read", true)).toEqual(["read", "write"]);
|
|
303
|
+
// Test with duplicates (explicitly set to keep)
|
|
304
|
+
expect(extractScopes("read write read", false)).toEqual(["read", "write", "read"]);
|
|
305
|
+
});
|
|
138
306
|
export function mergeScopeStrings(...scopes) {
|
|
139
307
|
// TODO what is this for? can we move this into the OAuth code in the backend?
|
|
140
308
|
const allScope = scopes.map((s) => extractScopes(s)).flat().join(" ");
|
|
141
309
|
return extractScopes(allScope).join(" ");
|
|
142
310
|
}
|
|
311
|
+
import.meta.vitest?.test("mergeScopeStrings", ({ expect }) => {
|
|
312
|
+
// Test with empty input
|
|
313
|
+
expect(mergeScopeStrings()).toBe("");
|
|
314
|
+
// Test with single scope string
|
|
315
|
+
expect(mergeScopeStrings("read write")).toBe("read write");
|
|
316
|
+
// Test with multiple scope strings
|
|
317
|
+
expect(mergeScopeStrings("read", "write")).toBe("read write");
|
|
318
|
+
// Test with overlapping scopes
|
|
319
|
+
expect(mergeScopeStrings("read write", "write delete")).toBe("read write delete");
|
|
320
|
+
// Test with extra whitespace
|
|
321
|
+
expect(mergeScopeStrings(" read write ", " delete ")).toBe("read write delete");
|
|
322
|
+
// Test with duplicates across strings
|
|
323
|
+
expect(mergeScopeStrings("read write", "write delete", "read")).toBe("read write delete");
|
|
324
|
+
// Test with empty strings
|
|
325
|
+
expect(mergeScopeStrings("read write", "", "delete")).toBe("read write delete");
|
|
326
|
+
});
|
|
143
327
|
export function escapeTemplateLiteral(s) {
|
|
144
328
|
return s.replaceAll("`", "\\`").replaceAll("\\", "\\\\").replaceAll("$", "\\$");
|
|
145
329
|
}
|
|
330
|
+
import.meta.vitest?.test("escapeTemplateLiteral", ({ expect }) => {
|
|
331
|
+
// Test with empty string
|
|
332
|
+
expect(escapeTemplateLiteral("")).toBe("");
|
|
333
|
+
// Test with normal string (no special characters)
|
|
334
|
+
expect(escapeTemplateLiteral("hello world")).toBe("hello world");
|
|
335
|
+
// Test with backtick
|
|
336
|
+
const input1 = "hello `world`";
|
|
337
|
+
const output1 = escapeTemplateLiteral(input1);
|
|
338
|
+
// Verify backticks are escaped
|
|
339
|
+
expect(output1.includes("\\`")).toBe(true);
|
|
340
|
+
expect(output1).not.toBe(input1);
|
|
341
|
+
// Test with backslash
|
|
342
|
+
const input2 = "hello \\world";
|
|
343
|
+
const output2 = escapeTemplateLiteral(input2);
|
|
344
|
+
// Verify backslashes are escaped
|
|
345
|
+
expect(output2.includes("\\\\")).toBe(true);
|
|
346
|
+
expect(output2).not.toBe(input2);
|
|
347
|
+
// Test with dollar sign
|
|
348
|
+
const input3 = "hello $world";
|
|
349
|
+
const output3 = escapeTemplateLiteral(input3);
|
|
350
|
+
// Verify dollar signs are escaped
|
|
351
|
+
expect(output3.includes("\\$")).toBe(true);
|
|
352
|
+
expect(output3).not.toBe(input3);
|
|
353
|
+
// Test with multiple special characters
|
|
354
|
+
const input4 = "`hello` $world\\";
|
|
355
|
+
const output4 = escapeTemplateLiteral(input4);
|
|
356
|
+
// Verify all special characters are escaped
|
|
357
|
+
expect(output4.includes("\\`")).toBe(true);
|
|
358
|
+
expect(output4.includes("\\$")).toBe(true);
|
|
359
|
+
expect(output4.includes("\\\\")).toBe(true);
|
|
360
|
+
expect(output4).not.toBe(input4);
|
|
361
|
+
// Test with already escaped characters
|
|
362
|
+
const input5 = "\\`hello\\`";
|
|
363
|
+
const output5 = escapeTemplateLiteral(input5);
|
|
364
|
+
// Verify already escaped characters are properly escaped
|
|
365
|
+
expect(output5).not.toBe(input5);
|
|
366
|
+
});
|
|
146
367
|
/**
|
|
147
368
|
* Some classes have different constructor names in different environments (eg. `Headers` is sometimes called `_Headers`,
|
|
148
369
|
* so we create an object of overrides to handle these cases.
|
package/dist/utils/unicode.js
CHANGED
|
@@ -8,3 +8,16 @@ export function getFlagEmoji(twoLetterCountryCode) {
|
|
|
8
8
|
.map(char => 127397 + char.charCodeAt(0));
|
|
9
9
|
return String.fromCodePoint(...codePoints);
|
|
10
10
|
}
|
|
11
|
+
import.meta.vitest?.test("getFlagEmoji", ({ expect }) => {
|
|
12
|
+
// Test with valid country codes
|
|
13
|
+
expect(getFlagEmoji("US")).toBe("πΊπΈ");
|
|
14
|
+
expect(getFlagEmoji("us")).toBe("πΊπΈ");
|
|
15
|
+
expect(getFlagEmoji("GB")).toBe("π¬π§");
|
|
16
|
+
expect(getFlagEmoji("JP")).toBe("π―π΅");
|
|
17
|
+
// Test with invalid country codes
|
|
18
|
+
expect(() => getFlagEmoji("")).toThrow("Country code must be two alphabetical letters");
|
|
19
|
+
expect(() => getFlagEmoji("A")).toThrow("Country code must be two alphabetical letters");
|
|
20
|
+
expect(() => getFlagEmoji("ABC")).toThrow("Country code must be two alphabetical letters");
|
|
21
|
+
expect(() => getFlagEmoji("12")).toThrow("Country code must be two alphabetical letters");
|
|
22
|
+
expect(() => getFlagEmoji("A1")).toThrow("Country code must be two alphabetical letters");
|
|
23
|
+
});
|