@stackframe/stack-shared 2.7.20 → 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 +6 -0
- package/dist/crud.js +76 -0
- package/dist/hooks/use-strict-memo.js +75 -0
- package/dist/known-errors.js +1 -1
- package/dist/utils/arrays.js +75 -1
- package/dist/utils/caches.js +33 -0
- package/dist/utils/compile-time.js +17 -0
- package/dist/utils/dates.js +4 -4
- 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/objects.js +69 -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 +1 -1
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
|
+
});
|
package/dist/utils/urls.js
CHANGED
|
@@ -8,15 +8,47 @@ export function createUrlIfValid(...args) {
|
|
|
8
8
|
return null;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
import.meta.vitest?.test("createUrlIfValid", ({ expect }) => {
|
|
12
|
+
// Test with valid URLs
|
|
13
|
+
expect(createUrlIfValid("https://example.com")).toBeInstanceOf(URL);
|
|
14
|
+
expect(createUrlIfValid("https://example.com/path?query=value#hash")).toBeInstanceOf(URL);
|
|
15
|
+
expect(createUrlIfValid("/path", "https://example.com")).toBeInstanceOf(URL);
|
|
16
|
+
// Test with invalid URLs
|
|
17
|
+
expect(createUrlIfValid("")).toBeNull();
|
|
18
|
+
expect(createUrlIfValid("not a url")).toBeNull();
|
|
19
|
+
expect(createUrlIfValid("http://")).toBeNull();
|
|
20
|
+
});
|
|
11
21
|
export function isValidUrl(url) {
|
|
12
22
|
return !!createUrlIfValid(url);
|
|
13
23
|
}
|
|
24
|
+
import.meta.vitest?.test("isValidUrl", ({ expect }) => {
|
|
25
|
+
// Test with valid URLs
|
|
26
|
+
expect(isValidUrl("https://example.com")).toBe(true);
|
|
27
|
+
expect(isValidUrl("http://localhost:3000")).toBe(true);
|
|
28
|
+
expect(isValidUrl("ftp://example.com")).toBe(true);
|
|
29
|
+
// Test with invalid URLs
|
|
30
|
+
expect(isValidUrl("")).toBe(false);
|
|
31
|
+
expect(isValidUrl("not a url")).toBe(false);
|
|
32
|
+
expect(isValidUrl("http://")).toBe(false);
|
|
33
|
+
});
|
|
14
34
|
export function isValidHostname(hostname) {
|
|
15
35
|
const url = createUrlIfValid(`https://${hostname}`);
|
|
16
36
|
if (!url)
|
|
17
37
|
return false;
|
|
18
38
|
return url.hostname === hostname;
|
|
19
39
|
}
|
|
40
|
+
import.meta.vitest?.test("isValidHostname", ({ expect }) => {
|
|
41
|
+
// Test with valid hostnames
|
|
42
|
+
expect(isValidHostname("example.com")).toBe(true);
|
|
43
|
+
expect(isValidHostname("localhost")).toBe(true);
|
|
44
|
+
expect(isValidHostname("sub.domain.example.com")).toBe(true);
|
|
45
|
+
expect(isValidHostname("127.0.0.1")).toBe(true);
|
|
46
|
+
// Test with invalid hostnames
|
|
47
|
+
expect(isValidHostname("")).toBe(false);
|
|
48
|
+
expect(isValidHostname("example.com/path")).toBe(false);
|
|
49
|
+
expect(isValidHostname("https://example.com")).toBe(false);
|
|
50
|
+
expect(isValidHostname("example com")).toBe(false);
|
|
51
|
+
});
|
|
20
52
|
export function isLocalhost(urlOrString) {
|
|
21
53
|
const url = createUrlIfValid(urlOrString);
|
|
22
54
|
if (!url)
|
|
@@ -27,6 +59,24 @@ export function isLocalhost(urlOrString) {
|
|
|
27
59
|
return true;
|
|
28
60
|
return false;
|
|
29
61
|
}
|
|
62
|
+
import.meta.vitest?.test("isLocalhost", ({ expect }) => {
|
|
63
|
+
// Test with localhost URLs
|
|
64
|
+
expect(isLocalhost("http://localhost")).toBe(true);
|
|
65
|
+
expect(isLocalhost("https://localhost:8080")).toBe(true);
|
|
66
|
+
expect(isLocalhost("http://sub.localhost")).toBe(true);
|
|
67
|
+
expect(isLocalhost("http://127.0.0.1")).toBe(true);
|
|
68
|
+
expect(isLocalhost("http://127.1.2.3")).toBe(true);
|
|
69
|
+
// Test with non-localhost URLs
|
|
70
|
+
expect(isLocalhost("https://example.com")).toBe(false);
|
|
71
|
+
expect(isLocalhost("http://192.168.1.1")).toBe(false);
|
|
72
|
+
expect(isLocalhost("http://10.0.0.1")).toBe(false);
|
|
73
|
+
// Test with URL objects
|
|
74
|
+
expect(isLocalhost(new URL("http://localhost"))).toBe(true);
|
|
75
|
+
expect(isLocalhost(new URL("https://example.com"))).toBe(false);
|
|
76
|
+
// Test with invalid URLs
|
|
77
|
+
expect(isLocalhost("not a url")).toBe(false);
|
|
78
|
+
expect(isLocalhost("")).toBe(false);
|
|
79
|
+
});
|
|
30
80
|
export function isRelative(url) {
|
|
31
81
|
const randomDomain = `${generateSecureRandomString()}.stack-auth.example.com`;
|
|
32
82
|
const u = createUrlIfValid(url, `https://${randomDomain}`);
|
|
@@ -38,9 +88,37 @@ export function isRelative(url) {
|
|
|
38
88
|
return false;
|
|
39
89
|
return true;
|
|
40
90
|
}
|
|
91
|
+
import.meta.vitest?.test("isRelative", ({ expect }) => {
|
|
92
|
+
// We can't easily mock generateSecureRandomString in this context
|
|
93
|
+
// but we can still test the function's behavior
|
|
94
|
+
// Test with relative URLs
|
|
95
|
+
expect(isRelative("/")).toBe(true);
|
|
96
|
+
expect(isRelative("/path")).toBe(true);
|
|
97
|
+
expect(isRelative("/path?query=value#hash")).toBe(true);
|
|
98
|
+
// Test with absolute URLs
|
|
99
|
+
expect(isRelative("https://example.com")).toBe(false);
|
|
100
|
+
expect(isRelative("http://example.com")).toBe(false);
|
|
101
|
+
expect(isRelative("//example.com")).toBe(false);
|
|
102
|
+
// Note: The implementation treats empty strings and invalid URLs as relative
|
|
103
|
+
// This is because they can be resolved against a base URL
|
|
104
|
+
expect(isRelative("")).toBe(true);
|
|
105
|
+
expect(isRelative("not a url")).toBe(true);
|
|
106
|
+
});
|
|
41
107
|
export function getRelativePart(url) {
|
|
42
108
|
return url.pathname + url.search + url.hash;
|
|
43
109
|
}
|
|
110
|
+
import.meta.vitest?.test("getRelativePart", ({ expect }) => {
|
|
111
|
+
// Test with various URLs
|
|
112
|
+
expect(getRelativePart(new URL("https://example.com"))).toBe("/");
|
|
113
|
+
expect(getRelativePart(new URL("https://example.com/path"))).toBe("/path");
|
|
114
|
+
expect(getRelativePart(new URL("https://example.com/path?query=value"))).toBe("/path?query=value");
|
|
115
|
+
expect(getRelativePart(new URL("https://example.com/path#hash"))).toBe("/path#hash");
|
|
116
|
+
expect(getRelativePart(new URL("https://example.com/path?query=value#hash"))).toBe("/path?query=value#hash");
|
|
117
|
+
// Test with different domains but same paths
|
|
118
|
+
const url1 = new URL("https://example.com/path?query=value#hash");
|
|
119
|
+
const url2 = new URL("https://different.com/path?query=value#hash");
|
|
120
|
+
expect(getRelativePart(url1)).toBe(getRelativePart(url2));
|
|
121
|
+
});
|
|
44
122
|
/**
|
|
45
123
|
* A template literal tag that returns a URL.
|
|
46
124
|
*
|
|
@@ -49,6 +127,27 @@ export function getRelativePart(url) {
|
|
|
49
127
|
export function url(strings, ...values) {
|
|
50
128
|
return new URL(urlString(strings, ...values));
|
|
51
129
|
}
|
|
130
|
+
import.meta.vitest?.test("url", ({ expect }) => {
|
|
131
|
+
// Test with no interpolation
|
|
132
|
+
expect(url `https://example.com`).toBeInstanceOf(URL);
|
|
133
|
+
expect(url `https://example.com`.href).toBe("https://example.com/");
|
|
134
|
+
// Test with string interpolation
|
|
135
|
+
expect(url `https://example.com/${"path"}`).toBeInstanceOf(URL);
|
|
136
|
+
expect(url `https://example.com/${"path"}`.pathname).toBe("/path");
|
|
137
|
+
// Test with number interpolation
|
|
138
|
+
expect(url `https://example.com/${42}`).toBeInstanceOf(URL);
|
|
139
|
+
expect(url `https://example.com/${42}`.pathname).toBe("/42");
|
|
140
|
+
// Test with boolean interpolation
|
|
141
|
+
expect(url `https://example.com/${true}`).toBeInstanceOf(URL);
|
|
142
|
+
expect(url `https://example.com/${true}`.pathname).toBe("/true");
|
|
143
|
+
// Test with special characters in interpolation
|
|
144
|
+
expect(url `https://example.com/${"path with spaces"}`).toBeInstanceOf(URL);
|
|
145
|
+
expect(url `https://example.com/${"path with spaces"}`.pathname).toBe("/path%20with%20spaces");
|
|
146
|
+
// Test with multiple interpolations
|
|
147
|
+
expect(url `https://example.com/${"path"}?query=${"value"}`).toBeInstanceOf(URL);
|
|
148
|
+
expect(url `https://example.com/${"path"}?query=${"value"}`.pathname).toBe("/path");
|
|
149
|
+
expect(url `https://example.com/${"path"}?query=${"value"}`.search).toBe("?query=value");
|
|
150
|
+
});
|
|
52
151
|
/**
|
|
53
152
|
* A template literal tag that returns a URL string.
|
|
54
153
|
*
|
|
@@ -57,3 +156,19 @@ export function url(strings, ...values) {
|
|
|
57
156
|
export function urlString(strings, ...values) {
|
|
58
157
|
return templateIdentity(strings, ...values.map(encodeURIComponent));
|
|
59
158
|
}
|
|
159
|
+
import.meta.vitest?.test("urlString", ({ expect }) => {
|
|
160
|
+
// Test with no interpolation
|
|
161
|
+
expect(urlString `https://example.com`).toBe("https://example.com");
|
|
162
|
+
// Test with string interpolation
|
|
163
|
+
expect(urlString `https://example.com/${"path"}`).toBe("https://example.com/path");
|
|
164
|
+
// Test with number interpolation
|
|
165
|
+
expect(urlString `https://example.com/${42}`).toBe("https://example.com/42");
|
|
166
|
+
// Test with boolean interpolation
|
|
167
|
+
expect(urlString `https://example.com/${true}`).toBe("https://example.com/true");
|
|
168
|
+
// Test with special characters in interpolation
|
|
169
|
+
expect(urlString `https://example.com/${"path with spaces"}`).toBe("https://example.com/path%20with%20spaces");
|
|
170
|
+
expect(urlString `https://example.com/${"?&="}`).toBe("https://example.com/%3F%26%3D");
|
|
171
|
+
// Test with multiple interpolations
|
|
172
|
+
expect(urlString `https://example.com/${"path"}?query=${"value"}`).toBe("https://example.com/path?query=value");
|
|
173
|
+
expect(urlString `https://example.com/${"path"}?query=${"value with spaces"}`).toBe("https://example.com/path?query=value%20with%20spaces");
|
|
174
|
+
});
|
package/dist/utils/uuids.js
CHANGED
|
@@ -3,6 +3,36 @@ export function generateUuid() {
|
|
|
3
3
|
// crypto.randomUuid is not supported in all browsers, so this is a polyfill
|
|
4
4
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (+c ^ generateRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));
|
|
5
5
|
}
|
|
6
|
+
import.meta.vitest?.test("generateUuid", ({ expect }) => {
|
|
7
|
+
// Test that the function returns a valid UUID
|
|
8
|
+
const uuid = generateUuid();
|
|
9
|
+
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
10
|
+
// Test that multiple calls generate different UUIDs
|
|
11
|
+
const uuid2 = generateUuid();
|
|
12
|
+
expect(uuid).not.toBe(uuid2);
|
|
13
|
+
// Test that the UUID is version 4 (random)
|
|
14
|
+
expect(uuid.charAt(14)).toBe('4');
|
|
15
|
+
// Test that the UUID has the correct variant (8, 9, a, or b in position 19)
|
|
16
|
+
expect('89ab').toContain(uuid.charAt(19));
|
|
17
|
+
});
|
|
6
18
|
export function isUuid(str) {
|
|
7
19
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(str);
|
|
8
20
|
}
|
|
21
|
+
import.meta.vitest?.test("isUuid", ({ expect }) => {
|
|
22
|
+
// Test with valid UUIDs
|
|
23
|
+
expect(isUuid("123e4567-e89b-42d3-a456-426614174000")).toBe(true);
|
|
24
|
+
expect(isUuid("123e4567-e89b-42d3-8456-426614174000")).toBe(true);
|
|
25
|
+
expect(isUuid("123e4567-e89b-42d3-9456-426614174000")).toBe(true);
|
|
26
|
+
expect(isUuid("123e4567-e89b-42d3-a456-426614174000")).toBe(true);
|
|
27
|
+
expect(isUuid("123e4567-e89b-42d3-b456-426614174000")).toBe(true);
|
|
28
|
+
// Test with invalid UUIDs
|
|
29
|
+
expect(isUuid("")).toBe(false);
|
|
30
|
+
expect(isUuid("not-a-uuid")).toBe(false);
|
|
31
|
+
expect(isUuid("123e4567-e89b-12d3-a456-426614174000")).toBe(false); // Wrong version (not 4)
|
|
32
|
+
expect(isUuid("123e4567-e89b-42d3-c456-426614174000")).toBe(false); // Wrong variant (not 8, 9, a, or b)
|
|
33
|
+
expect(isUuid("123e4567-e89b-42d3-a456-42661417400")).toBe(false); // Too short
|
|
34
|
+
expect(isUuid("123e4567-e89b-42d3-a456-4266141740000")).toBe(false); // Too long
|
|
35
|
+
expect(isUuid("123e4567-e89b-42d3-a456_426614174000")).toBe(false); // Wrong format (underscore instead of dash)
|
|
36
|
+
// Test with uppercase letters (should fail as UUID should be lowercase)
|
|
37
|
+
expect(isUuid("123E4567-E89B-42D3-A456-426614174000")).toBe(false);
|
|
38
|
+
});
|