@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.
@@ -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.
@@ -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
+ });
@@ -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
+ });
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.7.20",
3
+ "version": "2.7.21",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",