@heyramzi/utils 0.0.0-stage → 1.12.0

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/package.json +103 -4
  4. package/src/ai/image-agnes.test.ts +130 -0
  5. package/src/ai/image.ts +435 -0
  6. package/src/ai/models.test.ts +98 -0
  7. package/src/ai/models.ts +450 -0
  8. package/src/analytics/client.test.ts +149 -0
  9. package/src/analytics/client.ts +129 -0
  10. package/src/analytics/server.test.ts +165 -0
  11. package/src/analytics/server.ts +88 -0
  12. package/src/analytics/svelte.test.ts +105 -0
  13. package/src/analytics/svelte.ts +58 -0
  14. package/src/analytics/types.ts +26 -0
  15. package/src/analytics/umami/protocol.test.ts +18 -0
  16. package/src/analytics/umami/protocol.ts +104 -0
  17. package/src/analytics/umami/reporting.test.ts +391 -0
  18. package/src/analytics/umami/reporting.ts +627 -0
  19. package/src/auth/config/otp.ts +8 -0
  20. package/src/auth/config/rate-limits.ts +11 -0
  21. package/src/auth/config/trusted-origins.ts +32 -0
  22. package/src/auth/email/index.ts +7 -0
  23. package/src/auth/email/layout.test.ts +73 -0
  24. package/src/auth/email/layout.ts +122 -0
  25. package/src/auth/email/resend.test.ts +35 -0
  26. package/src/auth/email/resend.ts +26 -0
  27. package/src/auth/email/template.ts +77 -0
  28. package/src/auth/email/types.ts +74 -0
  29. package/src/auth/index.ts +5 -0
  30. package/src/cache.test.ts +67 -0
  31. package/src/cache.ts +5 -0
  32. package/src/cn.test.ts +52 -0
  33. package/src/cn.ts +16 -0
  34. package/src/collection.test.ts +66 -0
  35. package/src/collection.ts +8 -0
  36. package/src/crypto.test.ts +86 -0
  37. package/src/crypto.ts +157 -0
  38. package/src/error.test.ts +79 -0
  39. package/src/error.ts +16 -0
  40. package/src/errors/sentry/index.ts +20 -0
  41. package/src/errors/sentry/protocol.test.ts +85 -0
  42. package/src/errors/sentry/protocol.ts +144 -0
  43. package/src/errors/sentry/sveltekit.test.ts +88 -0
  44. package/src/errors/sentry/sveltekit.ts +188 -0
  45. package/src/format.test.ts +225 -0
  46. package/src/format.ts +103 -0
  47. package/src/grab.d.ts +2 -0
  48. package/src/grab.js +112 -0
  49. package/src/index.ts +38 -0
  50. package/src/logger-types.ts +14 -0
  51. package/src/logger.ts +24 -0
  52. package/src/math.test.ts +41 -0
  53. package/src/math.ts +3 -0
  54. package/src/og/fonts.ts +69 -0
  55. package/src/og/index.ts +5 -0
  56. package/src/og/markup.test.ts +73 -0
  57. package/src/og/markup.ts +90 -0
  58. package/src/og/renderer.ts +42 -0
  59. package/src/og/types.ts +26 -0
  60. package/src/og/wawoff2.d.ts +6 -0
  61. package/src/package-imports.test.mjs +22 -0
  62. package/src/result.test.ts +149 -0
  63. package/src/result.ts +29 -0
  64. package/src/string.test.ts +205 -0
  65. package/src/string.ts +71 -0
  66. package/src/testimonials/render-content.ts +21 -0
  67. package/src/testimonials/server.ts +47 -0
  68. package/src/testimonials/types.ts +35 -0
  69. package/src/timing.test.ts +166 -0
  70. package/src/timing.ts +28 -0
  71. package/src/vite.d.ts +12 -0
  72. package/src/vite.js +265 -0
  73. package/src/vite.test.js +235 -0
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect } from "vite-plus/test";
2
+ import {
3
+ slugify,
4
+ generateSlug,
5
+ buildSearchParams,
6
+ parseJSON,
7
+ parseCommaSeparatedList,
8
+ matchesQuery,
9
+ } from "#src/string.ts";
10
+
11
+ describe("slugify", () => {
12
+ it("lowercases the text", () => {
13
+ expect(slugify("Hello World")).toBe("hello-world");
14
+ });
15
+
16
+ it("replaces spaces with hyphens", () => {
17
+ expect(slugify("foo bar baz")).toBe("foo-bar-baz");
18
+ });
19
+
20
+ it("removes special characters", () => {
21
+ expect(slugify("Hello, World!")).toBe("hello-world");
22
+ });
23
+
24
+ it("collapses multiple spaces into a single hyphen", () => {
25
+ expect(slugify("foo bar")).toBe("foo-bar");
26
+ });
27
+
28
+ it("collapses multiple hyphens into one", () => {
29
+ expect(slugify("foo---bar")).toBe("foo-bar");
30
+ });
31
+
32
+ it("handles an empty string", () => {
33
+ expect(slugify("")).toBe("");
34
+ });
35
+
36
+ it("preserves existing hyphens", () => {
37
+ expect(slugify("hello-world")).toBe("hello-world");
38
+ });
39
+ });
40
+
41
+ describe("generateSlug", () => {
42
+ it("lowercases text", () => {
43
+ expect(generateSlug("Hello World")).toBe("hello-world");
44
+ });
45
+
46
+ it("replaces non-alphanumeric characters with hyphens", () => {
47
+ expect(generateSlug("Hello, World!")).toBe("hello-world");
48
+ });
49
+
50
+ it("strips leading and trailing hyphens", () => {
51
+ expect(generateSlug("!hello world!")).toBe("hello-world");
52
+ });
53
+
54
+ it("collapses runs of non-alphanumeric characters to a single hyphen", () => {
55
+ expect(generateSlug("foo --- bar")).toBe("foo-bar");
56
+ });
57
+
58
+ it("handles an empty string", () => {
59
+ expect(generateSlug("")).toBe("");
60
+ });
61
+
62
+ it("handles strings that are all special characters", () => {
63
+ expect(generateSlug("!!!")).toBe("");
64
+ });
65
+
66
+ it("preserves numbers", () => {
67
+ expect(generateSlug("Article 42")).toBe("article-42");
68
+ });
69
+ });
70
+
71
+ describe("buildSearchParams", () => {
72
+ it("returns empty string when no params", () => {
73
+ expect(buildSearchParams({})).toBe("");
74
+ });
75
+
76
+ it("omits undefined values", () => {
77
+ expect(buildSearchParams({ a: "1", b: undefined })).toBe("?a=1");
78
+ });
79
+
80
+ it("returns empty string when all values are undefined", () => {
81
+ expect(buildSearchParams({ a: undefined, b: undefined })).toBe("");
82
+ });
83
+
84
+ it("builds a query string from a single param", () => {
85
+ expect(buildSearchParams({ page: "1" })).toBe("?page=1");
86
+ });
87
+
88
+ it("builds a query string from multiple params", () => {
89
+ const result = buildSearchParams({ page: "1", limit: "10" });
90
+ expect(result).toBe("?page=1&limit=10");
91
+ });
92
+
93
+ it("URL-encodes values with special characters", () => {
94
+ const result = buildSearchParams({ q: "hello world" });
95
+ expect(result).toBe("?q=hello%20world");
96
+ });
97
+
98
+ it("encodes special characters in values", () => {
99
+ const result = buildSearchParams({ filter: "name=test" });
100
+ expect(result).toContain("filter=");
101
+ expect(result).toContain("name");
102
+ });
103
+ });
104
+
105
+ describe("parseJSON", () => {
106
+ it("parses a valid JSON string", () => {
107
+ expect(parseJSON('{"key":"value"}')).toEqual({ key: "value" });
108
+ });
109
+
110
+ it("returns undefined for invalid JSON", () => {
111
+ expect(parseJSON("not json")).toBeUndefined();
112
+ });
113
+
114
+ it("returns undefined for an empty string", () => {
115
+ expect(parseJSON("")).toBeUndefined();
116
+ });
117
+
118
+ it("parses a JSON array", () => {
119
+ expect(parseJSON("[1,2,3]")).toEqual([1, 2, 3]);
120
+ });
121
+
122
+ it("parses a JSON number", () => {
123
+ expect(parseJSON("42")).toBe(42);
124
+ });
125
+
126
+ it("parses a JSON boolean", () => {
127
+ expect(parseJSON("true")).toBe(true);
128
+ });
129
+
130
+ it("returns undefined for truncated JSON", () => {
131
+ expect(parseJSON('{"key":')).toBeUndefined();
132
+ });
133
+
134
+ it("accepts a type parameter for typed output", () => {
135
+ type User = { name: string; age: number };
136
+ const result = parseJSON<User>('{"name":"Alice","age":30}');
137
+ expect(result).toEqual({ name: "Alice", age: 30 });
138
+ });
139
+ });
140
+
141
+ describe("parseCommaSeparatedList", () => {
142
+ it("splits a comma-separated string into an array", () => {
143
+ expect(parseCommaSeparatedList("a,b,c")).toEqual(["a", "b", "c"]);
144
+ });
145
+
146
+ it("trims whitespace from each item", () => {
147
+ expect(parseCommaSeparatedList("a , b , c")).toEqual(["a", "b", "c"]);
148
+ });
149
+
150
+ it("filters out empty items", () => {
151
+ expect(parseCommaSeparatedList("a,,b")).toEqual(["a", "b"]);
152
+ });
153
+
154
+ it("returns an empty array for undefined", () => {
155
+ expect(parseCommaSeparatedList(undefined)).toEqual([]);
156
+ });
157
+
158
+ it("returns an empty array for an empty string", () => {
159
+ expect(parseCommaSeparatedList("")).toEqual([]);
160
+ });
161
+
162
+ it("returns a single-item array for a string with no commas", () => {
163
+ expect(parseCommaSeparatedList("hello")).toEqual(["hello"]);
164
+ });
165
+
166
+ it("handles commas at the start and end", () => {
167
+ expect(parseCommaSeparatedList(",a,b,")).toEqual(["a", "b"]);
168
+ });
169
+ });
170
+
171
+ describe("matchesQuery", () => {
172
+ it("matches a plain substring", () => {
173
+ expect(matchesQuery("Agency glue post 001", "glue")).toBe(true);
174
+ });
175
+
176
+ it("matches every word wherever it sits, not one run of characters", () => {
177
+ expect(matchesQuery("Agency glue post 001", "agency post")).toBe(true);
178
+ });
179
+
180
+ it("rejects a query whose second word is absent", () => {
181
+ expect(matchesQuery("Agency glue post 001", "agency draft")).toBe(false);
182
+ });
183
+
184
+ it("ignores case", () => {
185
+ expect(matchesQuery("Agency Glue", "AGENCY glue")).toBe(true);
186
+ });
187
+
188
+ it("ignores accents on both sides", () => {
189
+ expect(matchesQuery("São Paulo", "sao")).toBe(true);
190
+ expect(matchesQuery("Sao Paulo", "são")).toBe(true);
191
+ });
192
+
193
+ it("matches everything on a blank query", () => {
194
+ expect(matchesQuery("anything", "")).toBe(true);
195
+ expect(matchesQuery("anything", " ")).toBe(true);
196
+ });
197
+
198
+ it("ignores the spacing around the words", () => {
199
+ expect(matchesQuery("Agency glue post", " post agency ")).toBe(true);
200
+ });
201
+
202
+ it("finds nothing in an empty haystack", () => {
203
+ expect(matchesQuery("", "post")).toBe(false);
204
+ });
205
+ });
package/src/string.ts ADDED
@@ -0,0 +1,71 @@
1
+ /** Convert text to URL-safe slug */
2
+ export function slugify(text: string): string {
3
+ return text
4
+ .toLowerCase()
5
+ .replace(/[^\w\s-]/g, "")
6
+ .replace(/\s+/g, "-")
7
+ .replace(/-+/g, "-")
8
+ .trim();
9
+ }
10
+
11
+ /** Generate a URL-safe slug (strips non-alphanumeric, trims hyphens) */
12
+ export function generateSlug(text: string): string {
13
+ return text
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, "-")
16
+ .replace(/^-|-$/g, "");
17
+ }
18
+
19
+ /** Build query string from params, omitting undefined values */
20
+ export function buildSearchParams(params: Record<string, string | undefined>): string {
21
+ const entries = Object.entries(params).filter(
22
+ (entry): entry is [string, string] => entry[1] !== undefined,
23
+ );
24
+ if (entries.length === 0) return "";
25
+ return `?${entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&")}`;
26
+ }
27
+
28
+ /** Safely parse JSON string, returning undefined on failure */
29
+ export function parseJSON<T = unknown>(raw: string): T | undefined {
30
+ if (!raw) return undefined;
31
+ try {
32
+ return JSON.parse(raw) as T;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ /** Split comma-separated string into trimmed, non-empty array */
39
+ export function parseCommaSeparatedList(text: string | undefined): string[] {
40
+ if (!text) return [];
41
+ return text
42
+ .split(",")
43
+ .map((s) => s.trim())
44
+ .filter(Boolean);
45
+ }
46
+
47
+ /** Lowercase and strip accents, so a search for "sao" reaches "São". */
48
+ function foldForSearch(text: string): string {
49
+ return text
50
+ .normalize("NFD")
51
+ .replace(/\p{Diacritic}/gu, "")
52
+ .toLowerCase();
53
+ }
54
+
55
+ /**
56
+ * True when every word of `query` appears somewhere in `haystack`, accents and case ignored. An empty or blank query matches everything.
57
+ */
58
+
59
+ /*
60
+ * WHY word by word and not one substring test: a list page joins the fields it searches into one string, so a plain `includes` makes the field order part of
61
+ */
62
+
63
+ /*
64
+ * the query. On a set titled "Agency glue post 001", typing "agency post" matched nothing, which reads as the search being broken rather than strict.
65
+ */
66
+ export function matchesQuery(haystack: string, query: string): boolean {
67
+ const words = foldForSearch(query).split(/\s+/).filter(Boolean);
68
+ if (words.length === 0) return true;
69
+ const target = foldForSearch(haystack);
70
+ return words.every((word) => target.includes(word));
71
+ }
@@ -0,0 +1,21 @@
1
+ // Safe converter for testimonial content stored with **bold** markdown.
2
+ // Escapes HTML, then replaces **...** with <strong>...</strong>.
3
+ // Output is intended for {@html ...} or dangerouslySetInnerHTML.
4
+
5
+ const HTML_ESCAPES: Record<string, string> = {
6
+ "&": "&amp;",
7
+ "<": "&lt;",
8
+ ">": "&gt;",
9
+ '"': "&quot;",
10
+ "'": "&#39;",
11
+ };
12
+
13
+ function escapeHtml(input: string): string {
14
+ return input.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
15
+ }
16
+
17
+ export function renderTestimonialContent(content: string | null | undefined): string {
18
+ if (!content) return "";
19
+ const escaped = escapeHtml(content);
20
+ return escaped.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
21
+ }
@@ -0,0 +1,47 @@
1
+ import type {
2
+ PublicCollectionResponse,
3
+ PublicTestimonial,
4
+ CollectionStat,
5
+ } from "#src/testimonials/types.js";
6
+
7
+ export const DEFAULT_TESTIMONIALS_BASE_URL = "https://app.upsys-consulting.com";
8
+
9
+ export type FetchPublicTestimonialsOptions = {
10
+ baseUrl?: string;
11
+ locale?: "en" | "fr";
12
+ layout?: string;
13
+ fetchImpl?: typeof fetch;
14
+ };
15
+
16
+ export async function fetchPublicTestimonialCollection(
17
+ token: string,
18
+ opts: FetchPublicTestimonialsOptions = {},
19
+ ): Promise<PublicCollectionResponse | null> {
20
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
21
+ const baseUrl = (opts.baseUrl ?? DEFAULT_TESTIMONIALS_BASE_URL).replace(/\/$/, "");
22
+ const params = new URLSearchParams();
23
+ if (opts.locale) params.set("locale", opts.locale);
24
+ if (opts.layout) params.set("layout", opts.layout);
25
+ const query = params.toString();
26
+ const url = `${baseUrl}/api/testimonials/public/${encodeURIComponent(token)}${query ? `?${query}` : ""}`;
27
+
28
+ const response = await fetchImpl(url);
29
+ if (!response.ok) return null;
30
+ return (await response.json()) as PublicCollectionResponse;
31
+ }
32
+
33
+ export async function fetchPublicTestimonials(
34
+ token: string,
35
+ opts: FetchPublicTestimonialsOptions = {},
36
+ ): Promise<PublicTestimonial[]> {
37
+ const data = await fetchPublicTestimonialCollection(token, opts);
38
+ return data?.testimonials ?? [];
39
+ }
40
+
41
+ export async function fetchPublicTestimonialStats(
42
+ token: string,
43
+ opts: FetchPublicTestimonialsOptions = {},
44
+ ): Promise<CollectionStat[]> {
45
+ const data = await fetchPublicTestimonialCollection(token, opts);
46
+ return data?.collection.stats ?? [];
47
+ }
@@ -0,0 +1,35 @@
1
+ export type CollectionLayout = "grid" | "carousel" | "compact" | "highlight";
2
+
3
+ export type CollectionStat = {
4
+ number: string;
5
+ labelEn: string;
6
+ labelFr: string;
7
+ };
8
+
9
+ export type PublicTestimonial = {
10
+ id: string;
11
+ authorName: string;
12
+ authorRole: string | null;
13
+ authorCompany: string | null;
14
+ authorPhotoUrl: string | null;
15
+ content: string;
16
+ /** Curated pull-quote for surfaces with room for a line or two. Null when unset. */
17
+ excerpt: string | null;
18
+ rating: number | null;
19
+ videoUrl: string | null;
20
+ locale: "en" | "fr";
21
+ tags: string[];
22
+ isFeatured: boolean;
23
+ createdAt: string;
24
+ };
25
+
26
+ export type PublicCollectionResponse = {
27
+ collection: {
28
+ id: string;
29
+ name: string;
30
+ layout: CollectionLayout;
31
+ isPublic: boolean;
32
+ stats: CollectionStat[];
33
+ };
34
+ testimonials: PublicTestimonial[];
35
+ };
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vite-plus/test";
2
+ import { debounce, throttle, sleep } from "#src/timing.ts";
3
+
4
+ describe("debounce", () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ });
8
+
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+
13
+ it("does not call the function before the delay", () => {
14
+ const fn = vi.fn();
15
+ const debounced = debounce(fn, 100);
16
+
17
+ debounced();
18
+ expect(fn).not.toHaveBeenCalled();
19
+ });
20
+
21
+ it("calls the function after the delay", () => {
22
+ const fn = vi.fn();
23
+ const debounced = debounce(fn, 100);
24
+
25
+ debounced();
26
+ vi.advanceTimersByTime(100);
27
+
28
+ expect(fn).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ it("only calls once when invoked multiple times within the delay", () => {
32
+ const fn = vi.fn();
33
+ const debounced = debounce(fn, 100);
34
+
35
+ debounced();
36
+ debounced();
37
+ debounced();
38
+ vi.advanceTimersByTime(100);
39
+
40
+ expect(fn).toHaveBeenCalledTimes(1);
41
+ });
42
+
43
+ it("resets the timer on each call", () => {
44
+ const fn = vi.fn();
45
+ const debounced = debounce(fn, 100);
46
+
47
+ debounced();
48
+ vi.advanceTimersByTime(50);
49
+ debounced();
50
+ vi.advanceTimersByTime(50);
51
+
52
+ expect(fn).not.toHaveBeenCalled();
53
+
54
+ vi.advanceTimersByTime(50);
55
+ expect(fn).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it("forwards arguments to the wrapped function", () => {
59
+ const fn = vi.fn();
60
+ const debounced = debounce(fn, 100);
61
+
62
+ debounced("arg1", "arg2");
63
+ vi.advanceTimersByTime(100);
64
+
65
+ expect(fn).toHaveBeenCalledWith("arg1", "arg2");
66
+ });
67
+
68
+ it("can be called multiple times after each delay period", () => {
69
+ const fn = vi.fn();
70
+ const debounced = debounce(fn, 100);
71
+
72
+ debounced();
73
+ vi.advanceTimersByTime(100);
74
+ debounced();
75
+ vi.advanceTimersByTime(100);
76
+
77
+ expect(fn).toHaveBeenCalledTimes(2);
78
+ });
79
+ });
80
+
81
+ describe("throttle", () => {
82
+ beforeEach(() => {
83
+ vi.useFakeTimers();
84
+ });
85
+
86
+ afterEach(() => {
87
+ vi.useRealTimers();
88
+ });
89
+
90
+ it("calls the function immediately on the first invocation", () => {
91
+ const fn = vi.fn();
92
+ const throttled = throttle(fn, 100);
93
+
94
+ throttled();
95
+ expect(fn).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ it("does not call the function again within the limit", () => {
99
+ const fn = vi.fn();
100
+ const throttled = throttle(fn, 100);
101
+
102
+ throttled();
103
+ throttled();
104
+ throttled();
105
+
106
+ expect(fn).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it("allows a second call after the limit period has passed", () => {
110
+ const fn = vi.fn();
111
+ const throttled = throttle(fn, 100);
112
+
113
+ throttled();
114
+ vi.advanceTimersByTime(100);
115
+ throttled();
116
+
117
+ expect(fn).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ it("forwards arguments to the wrapped function", () => {
121
+ const fn = vi.fn();
122
+ const throttled = throttle(fn, 100);
123
+
124
+ throttled("hello", 42);
125
+ expect(fn).toHaveBeenCalledWith("hello", 42);
126
+ });
127
+ });
128
+
129
+ describe("sleep", () => {
130
+ beforeEach(() => {
131
+ vi.useFakeTimers();
132
+ });
133
+
134
+ afterEach(() => {
135
+ vi.useRealTimers();
136
+ });
137
+
138
+ it("returns a Promise", () => {
139
+ const result = sleep(100);
140
+ expect(result).toBeInstanceOf(Promise);
141
+ vi.runAllTimers();
142
+ });
143
+
144
+ it("resolves after the given number of milliseconds", async () => {
145
+ let resolved = false;
146
+ sleep(500).then(() => {
147
+ resolved = true;
148
+ });
149
+
150
+ expect(resolved).toBe(false);
151
+ vi.advanceTimersByTime(499);
152
+ await Promise.resolve(); // flush microtasks
153
+ expect(resolved).toBe(false);
154
+
155
+ vi.advanceTimersByTime(1);
156
+ await Promise.resolve();
157
+ expect(resolved).toBe(true);
158
+ });
159
+
160
+ it("resolves with undefined", async () => {
161
+ const promise = sleep(10);
162
+ vi.advanceTimersByTime(10);
163
+ const result = await promise;
164
+ expect(result).toBeUndefined();
165
+ });
166
+ });
package/src/timing.ts ADDED
@@ -0,0 +1,28 @@
1
+ export function debounce<T extends (...args: Parameters<T>) => ReturnType<T>>(
2
+ fn: T,
3
+ delay: number,
4
+ ): (...args: Parameters<T>) => void {
5
+ let timeoutId: ReturnType<typeof setTimeout>;
6
+ return (...args: Parameters<T>) => {
7
+ clearTimeout(timeoutId);
8
+ timeoutId = setTimeout(() => fn(...args), delay);
9
+ };
10
+ }
11
+
12
+ export function throttle<T extends (...args: Parameters<T>) => ReturnType<T>>(
13
+ fn: T,
14
+ limit: number,
15
+ ): (...args: Parameters<T>) => void {
16
+ let inThrottle = false;
17
+ return (...args: Parameters<T>) => {
18
+ if (!inThrottle) {
19
+ fn(...args);
20
+ inThrottle = true;
21
+ setTimeout(() => (inThrottle = false), limit);
22
+ }
23
+ };
24
+ }
25
+
26
+ export function sleep(ms: number): Promise<void> {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
package/src/vite.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ export declare function packageImportsOverKitAlias(): Plugin;
4
+
5
+ /**
6
+ * Vite plugin: before the dev server binds, kill whatever is already listening on the configured port and take the port back, instead of letting Vite drift to the next free
7
+ */
8
+
9
+ /*
10
+ * port. Dev-only, macOS/Linux only, no-op on Windows or any failure.
11
+ */
12
+ export declare function reclaimPort(): Plugin;