@heyramzi/utils 0.0.0-stage → 1.13.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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/dist/ai/image.js +220 -0
  4. package/dist/ai/models.js +261 -0
  5. package/dist/analytics/client.js +57 -0
  6. package/dist/analytics/server.js +54 -0
  7. package/dist/analytics/svelte.js +24 -0
  8. package/dist/analytics/types.js +0 -0
  9. package/dist/analytics/umami/protocol.js +35 -0
  10. package/dist/analytics/umami/reporting.js +189 -0
  11. package/dist/auth/config/otp.js +4 -0
  12. package/dist/auth/config/rate-limits.js +20 -0
  13. package/dist/auth/config/trusted-origins.js +15 -0
  14. package/dist/auth/email/index.js +3 -0
  15. package/dist/auth/email/layout.js +84 -0
  16. package/dist/auth/email/resend.js +30 -0
  17. package/dist/auth/email/template.js +57 -0
  18. package/dist/auth/email/types.js +0 -0
  19. package/dist/auth/index.js +3 -0
  20. package/dist/cache.js +4 -0
  21. package/dist/cn.js +5 -0
  22. package/dist/collection.js +8 -0
  23. package/dist/crypto.js +117 -0
  24. package/dist/error.js +15 -0
  25. package/dist/errors/sentry/index.js +15 -0
  26. package/dist/errors/sentry/protocol.js +130 -0
  27. package/dist/errors/sentry/sveltekit.js +145 -0
  28. package/dist/format.js +62 -0
  29. package/dist/grab.js +112 -0
  30. package/dist/index.js +10 -0
  31. package/dist/logger-types.js +0 -0
  32. package/dist/logger.js +22 -0
  33. package/dist/math.js +3 -0
  34. package/dist/og/fonts.js +46 -0
  35. package/dist/og/index.js +3 -0
  36. package/dist/og/markup.js +31 -0
  37. package/dist/og/renderer.js +29 -0
  38. package/dist/og/types.js +0 -0
  39. package/dist/result.js +26 -0
  40. package/dist/string.js +32 -0
  41. package/dist/testimonials/render-content.js +15 -0
  42. package/dist/testimonials/server.js +21 -0
  43. package/dist/testimonials/types.js +0 -0
  44. package/dist/timing.js +20 -0
  45. package/dist/vite.js +265 -0
  46. package/package.json +201 -4
  47. package/src/ai/image-agnes.test.ts +130 -0
  48. package/src/ai/image.ts +435 -0
  49. package/src/ai/models.test.ts +98 -0
  50. package/src/ai/models.ts +450 -0
  51. package/src/analytics/client.test.ts +149 -0
  52. package/src/analytics/client.ts +129 -0
  53. package/src/analytics/server.test.ts +165 -0
  54. package/src/analytics/server.ts +88 -0
  55. package/src/analytics/svelte.test.ts +105 -0
  56. package/src/analytics/svelte.ts +58 -0
  57. package/src/analytics/types.ts +26 -0
  58. package/src/analytics/umami/protocol.test.ts +18 -0
  59. package/src/analytics/umami/protocol.ts +104 -0
  60. package/src/analytics/umami/reporting.test.ts +391 -0
  61. package/src/analytics/umami/reporting.ts +627 -0
  62. package/src/auth/config/otp.ts +8 -0
  63. package/src/auth/config/rate-limits.ts +11 -0
  64. package/src/auth/config/trusted-origins.ts +32 -0
  65. package/src/auth/email/index.ts +7 -0
  66. package/src/auth/email/layout.test.ts +73 -0
  67. package/src/auth/email/layout.ts +122 -0
  68. package/src/auth/email/resend.test.ts +35 -0
  69. package/src/auth/email/resend.ts +26 -0
  70. package/src/auth/email/template.ts +77 -0
  71. package/src/auth/email/types.ts +74 -0
  72. package/src/auth/index.ts +5 -0
  73. package/src/cache.test.ts +67 -0
  74. package/src/cache.ts +5 -0
  75. package/src/cn.test.ts +52 -0
  76. package/src/cn.ts +16 -0
  77. package/src/collection.test.ts +66 -0
  78. package/src/collection.ts +8 -0
  79. package/src/crypto.test.ts +86 -0
  80. package/src/crypto.ts +157 -0
  81. package/src/error.test.ts +79 -0
  82. package/src/error.ts +16 -0
  83. package/src/errors/sentry/index.ts +20 -0
  84. package/src/errors/sentry/protocol.test.ts +85 -0
  85. package/src/errors/sentry/protocol.ts +144 -0
  86. package/src/errors/sentry/sveltekit.test.ts +149 -0
  87. package/src/errors/sentry/sveltekit.ts +226 -0
  88. package/src/format.test.ts +225 -0
  89. package/src/format.ts +103 -0
  90. package/src/grab.d.ts +2 -0
  91. package/src/grab.js +112 -0
  92. package/src/index.ts +38 -0
  93. package/src/logger-types.ts +14 -0
  94. package/src/logger.ts +24 -0
  95. package/src/math.test.ts +41 -0
  96. package/src/math.ts +3 -0
  97. package/src/og/fonts.ts +69 -0
  98. package/src/og/index.ts +5 -0
  99. package/src/og/markup.test.ts +73 -0
  100. package/src/og/markup.ts +90 -0
  101. package/src/og/renderer.ts +42 -0
  102. package/src/og/types.ts +26 -0
  103. package/src/og/wawoff2.d.ts +6 -0
  104. package/src/package-imports.test.mjs +22 -0
  105. package/src/result.test.ts +149 -0
  106. package/src/result.ts +29 -0
  107. package/src/string.test.ts +205 -0
  108. package/src/string.ts +71 -0
  109. package/src/testimonials/render-content.ts +21 -0
  110. package/src/testimonials/server.ts +47 -0
  111. package/src/testimonials/types.ts +35 -0
  112. package/src/timing.test.ts +166 -0
  113. package/src/timing.ts +28 -0
  114. package/src/vite.d.ts +12 -0
  115. package/src/vite.js +265 -0
  116. package/src/vite.test.js +235 -0
package/dist/crypto.js ADDED
@@ -0,0 +1,117 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
3
+ const ALGORITHM = "aes-256-gcm";
4
+ const AUTH_TAG_BYTES = 16;
5
+ const IV_BYTES = 12;
6
+ const KEY_BYTES = 32;
7
+ const KEY_ENV = "HEYRAMZI_UTILS_CRYPTO_KEY";
8
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
9
+ const HEX_PATTERN = /^[0-9a-fA-F]{64}$/;
10
+ function ensureServerEnv() {
11
+ const runtimeProcess = globalThis.process;
12
+ if (!runtimeProcess?.env) {
13
+ throw new Error("[crypto] process.env is unavailable. @heyramzi/utils/crypto is server-only.");
14
+ }
15
+ return runtimeProcess.env;
16
+ }
17
+ function decodeBase64(value, label) {
18
+ const normalized = value.trim();
19
+ if (!normalized || normalized.length % 4 !== 0 || !BASE64_PATTERN.test(normalized)) {
20
+ throw new Error(`[crypto] ${label} must be valid base64.`);
21
+ }
22
+ const bytes = Buffer.from(normalized, "base64");
23
+ if (bytes.toString("base64") !== normalized) {
24
+ throw new Error(`[crypto] ${label} must be valid base64.`);
25
+ }
26
+ return bytes;
27
+ }
28
+ function decodeSecretKey(rawValue, label) {
29
+ if (!rawValue) {
30
+ throw new Error(`[crypto] Missing ${label}. Set a 32-byte key encoded as base64 (recommended) or 64-character hex.`);
31
+ }
32
+ if (HEX_PATTERN.test(rawValue)) {
33
+ return Buffer.from(rawValue, "hex");
34
+ }
35
+ const key = decodeBase64(rawValue, label);
36
+ if (key.length !== KEY_BYTES) {
37
+ throw new Error(`[crypto] ${label} must decode to exactly ${KEY_BYTES} bytes for AES-256-GCM.`);
38
+ }
39
+ return key;
40
+ }
41
+ function getSecretKey() {
42
+ const env = ensureServerEnv();
43
+ const rawValue = env[KEY_ENV]?.trim();
44
+ return decodeSecretKey(rawValue ?? "", KEY_ENV);
45
+ }
46
+ function encryptWithKey(plaintext, key) {
47
+ const iv = randomBytes(IV_BYTES);
48
+ const cipher = createCipheriv(ALGORITHM, key, iv);
49
+ const ciphertext = Buffer.concat([
50
+ cipher.update(plaintext, "utf8"),
51
+ cipher.final(),
52
+ cipher.getAuthTag()
53
+ ]);
54
+ return {
55
+ ciphertext: ciphertext.toString("base64"),
56
+ iv: iv.toString("base64")
57
+ };
58
+ }
59
+ function decryptWithKey(ciphertext, iv, key) {
60
+ const ivBytes = decodeBase64(iv, "iv");
61
+ if (ivBytes.length !== IV_BYTES) {
62
+ throw new Error(`[crypto] iv must decode to exactly ${IV_BYTES} bytes.`);
63
+ }
64
+ const payload = decodeBase64(ciphertext, "ciphertext");
65
+ if (payload.length < AUTH_TAG_BYTES) {
66
+ throw new Error("[crypto] ciphertext payload is invalid.");
67
+ }
68
+ const encrypted = payload.subarray(0, -AUTH_TAG_BYTES);
69
+ const authTag = payload.subarray(-AUTH_TAG_BYTES);
70
+ try {
71
+ const decipher = createDecipheriv(ALGORITHM, key, ivBytes);
72
+ decipher.setAuthTag(authTag);
73
+ return Buffer.concat([
74
+ decipher.update(encrypted),
75
+ decipher.final()
76
+ ]).toString("utf8");
77
+ } catch (error) {
78
+ throw new Error("[crypto] Failed to decrypt secret. The payload may be tampered with or invalid.", {
79
+ cause: error
80
+ });
81
+ }
82
+ }
83
+ export function encryptSecret(plaintext) {
84
+ return encryptWithKey(plaintext, getSecretKey());
85
+ }
86
+ export function decryptSecret(ciphertext, iv) {
87
+ return decryptWithKey(ciphertext, iv, getSecretKey());
88
+ }
89
+ export function encryptSecretWithKey(plaintext, key) {
90
+ return encryptWithKey(plaintext, decodeSecretKey(key.trim(), "key"));
91
+ }
92
+ export function decryptSecretWithKey(ciphertext, iv, key) {
93
+ return decryptWithKey(ciphertext, iv, decodeSecretKey(key.trim(), "key"));
94
+ }
95
+ export function packSecret(secret) {
96
+ const ivBytes = decodeBase64(secret.iv, "iv");
97
+ if (ivBytes.length !== IV_BYTES) {
98
+ throw new Error(`[crypto] iv must decode to exactly ${IV_BYTES} bytes.`);
99
+ }
100
+ const ciphertextBytes = decodeBase64(secret.ciphertext, "ciphertext");
101
+ return Buffer.concat([
102
+ ivBytes,
103
+ ciphertextBytes
104
+ ]).toString("base64");
105
+ }
106
+ export function unpackSecret(payload) {
107
+ const packed = decodeBase64(payload, "payload");
108
+ if (packed.length < IV_BYTES + AUTH_TAG_BYTES) {
109
+ throw new Error("[crypto] packed payload is invalid.");
110
+ }
111
+ const iv = packed.subarray(0, IV_BYTES);
112
+ const ciphertext = packed.subarray(IV_BYTES);
113
+ return {
114
+ ciphertext: ciphertext.toString("base64"),
115
+ iv: iv.toString("base64")
116
+ };
117
+ }
package/dist/error.js ADDED
@@ -0,0 +1,15 @@
1
+ export async function fetchApi(url, options) {
2
+ const response = await fetch(url, {
3
+ headers: {
4
+ "Content-Type": "application/json",
5
+ ...options?.headers
6
+ },
7
+ ...options
8
+ });
9
+ const data = await response.json();
10
+ if (!response.ok) {
11
+ const message = data?.error ?? `Request failed (${response.status})`;
12
+ throw new Error(message);
13
+ }
14
+ return data;
15
+ }
@@ -0,0 +1,15 @@
1
+ import { buildErrorRequest } from "./protocol.js";
2
+ export { buildErrorRequest, parseDsn, parseStack } from "./protocol.js";
3
+ export async function captureError(input) {
4
+ const req = buildErrorRequest(input);
5
+ if (!req) return;
6
+ try {
7
+ await fetch(req.url, {
8
+ method: "POST",
9
+ headers: req.headers,
10
+ body: req.body
11
+ });
12
+ } catch (err) {
13
+ console.error("[errors] Sentry capture failed:", err);
14
+ }
15
+ }
@@ -0,0 +1,130 @@
1
+ export function parseDsn(dsn) {
2
+ const m = /^(https?):\/\/([^@/]+)@([^/]+)\/(?:.*\/)?(\d+)\/?$/.exec(dsn.trim());
3
+ const [, scheme, publicKey, host, projectId] = m ?? [];
4
+ if (!scheme || !publicKey || !host || !projectId) return null;
5
+ return {
6
+ publicKey,
7
+ origin: `${scheme}://${host}`,
8
+ projectId
9
+ };
10
+ }
11
+ function randomHex32() {
12
+ let out = "";
13
+ for(let i = 0; i < 32; i++)out += Math.floor(Math.random() * 16).toString(16);
14
+ return out;
15
+ }
16
+ const V8_FRAME = /^\s*at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?\s*$/;
17
+ export function parseStack(stack) {
18
+ if (!stack) return [];
19
+ const frames = [];
20
+ for (const line of stack.split("\n")){
21
+ const m = V8_FRAME.exec(line);
22
+ if (!m) continue;
23
+ const frame = {
24
+ filename: m[2],
25
+ lineno: Number(m[3]),
26
+ colno: Number(m[4])
27
+ };
28
+ if (m[1]) frame.function = m[1];
29
+ frames.push(frame);
30
+ }
31
+ return frames.reverse();
32
+ }
33
+ function describe(error) {
34
+ if (error instanceof Error) {
35
+ return {
36
+ type: error.name || "Error",
37
+ value: error.message,
38
+ stack: error.stack
39
+ };
40
+ }
41
+ if (typeof error === "string") return {
42
+ type: "Error",
43
+ value: error
44
+ };
45
+ try {
46
+ return {
47
+ type: "Error",
48
+ value: JSON.stringify(error)
49
+ };
50
+ } catch {
51
+ return {
52
+ type: "Error",
53
+ value: String(error)
54
+ };
55
+ }
56
+ }
57
+ const MAX_CHAIN = 5;
58
+ function describeChain(error) {
59
+ const chain = [
60
+ describe(error)
61
+ ];
62
+ const seen = new Set([
63
+ error
64
+ ]);
65
+ let next = error instanceof Error ? error.cause : undefined;
66
+ while(next !== undefined && next !== null && !seen.has(next) && chain.length < MAX_CHAIN){
67
+ seen.add(next);
68
+ chain.push(describe(next));
69
+ next = next instanceof Error ? next.cause : undefined;
70
+ }
71
+ return chain.reverse();
72
+ }
73
+ function cleanTags(input) {
74
+ const tags = {};
75
+ for (const [k, v] of Object.entries(input.tags ?? {})){
76
+ if (v !== null && v !== undefined) tags[k] = String(v);
77
+ }
78
+ if (input.surface) tags.surface = input.surface;
79
+ return tags;
80
+ }
81
+ export function buildErrorRequest(input) {
82
+ const dsn = parseDsn(input.dsn ?? "");
83
+ if (!dsn) return null;
84
+ const eventId = input.eventId ?? randomHex32();
85
+ const now = input.now ?? Date.now();
86
+ const values = describeChain(input.error).map(({ type, value, stack })=>{
87
+ const exception = {
88
+ type,
89
+ value
90
+ };
91
+ const frames = parseStack(stack);
92
+ if (frames.length) exception.stacktrace = {
93
+ frames
94
+ };
95
+ return exception;
96
+ });
97
+ const event = {
98
+ event_id: eventId,
99
+ timestamp: now / 1000,
100
+ platform: "javascript",
101
+ level: "error",
102
+ tags: cleanTags(input),
103
+ exception: {
104
+ values
105
+ }
106
+ };
107
+ if (input.release) event.release = input.release;
108
+ if (input.environment) event.environment = input.environment;
109
+ if (input.user) event.user = input.user;
110
+ if (input.extra) event.extra = input.extra;
111
+ const sentAt = new Date(now).toISOString();
112
+ const body = [
113
+ JSON.stringify({
114
+ event_id: eventId,
115
+ sent_at: sentAt,
116
+ dsn: input.dsn
117
+ }),
118
+ JSON.stringify({
119
+ type: "event"
120
+ }),
121
+ JSON.stringify(event)
122
+ ].join("\n");
123
+ return {
124
+ url: `${dsn.origin}/api/${dsn.projectId}/envelope/?sentry_key=${dsn.publicKey}&sentry_version=7`,
125
+ headers: {
126
+ "Content-Type": "application/x-sentry-envelope"
127
+ },
128
+ body
129
+ };
130
+ }
@@ -0,0 +1,145 @@
1
+ import { captureError } from "./index.js";
2
+ const LOCAL_HOSTS = new Set([
3
+ "localhost",
4
+ "127.0.0.1",
5
+ "[::1]"
6
+ ]);
7
+ export function shouldReport({ kind, error }) {
8
+ if (kind === "app") return false;
9
+ if (kind === "unknown") return true;
10
+ const status = error?.status;
11
+ return typeof status === "number" ? status >= 500 : true;
12
+ }
13
+ const STALE_CHUNK = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/;
14
+ export function isStaleChunk(error) {
15
+ const message = error?.message;
16
+ return typeof message === "string" && STALE_CHUNK.test(message);
17
+ }
18
+ const RELOAD_KEY = "heyramzi:stale-chunk-reload";
19
+ const RELOAD_WINDOW_MS = 30_000;
20
+ function browserEnv() {
21
+ try {
22
+ return {
23
+ storage: sessionStorage,
24
+ now: Date.now,
25
+ reload: ()=>location.reload()
26
+ };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ export function reloadForStaleChunk(env = browserEnv()) {
32
+ if (!env) return false;
33
+ try {
34
+ const last = Number(env.storage.getItem(RELOAD_KEY));
35
+ if (env.now() - last < RELOAD_WINDOW_MS) return false;
36
+ env.storage.setItem(RELOAD_KEY, String(env.now()));
37
+ env.reload();
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function toError(raw) {
44
+ if (raw instanceof Error) return raw;
45
+ const message = raw?.message;
46
+ return typeof message === "string" ? new Error(message) : raw;
47
+ }
48
+ const STACK_LINES = 8;
49
+ export function devErrorDetails(error) {
50
+ if (!(error instanceof Error)) {
51
+ const message = error?.message;
52
+ if (typeof message === "string") return {
53
+ message
54
+ };
55
+ try {
56
+ return {
57
+ message: typeof error === "object" ? JSON.stringify(error) : String(error)
58
+ };
59
+ } catch {
60
+ return {
61
+ message: String(error)
62
+ };
63
+ }
64
+ }
65
+ const header = String(error);
66
+ const stack = error.stack?.startsWith(header) ? error.stack.slice(header.length) : error.stack;
67
+ const frames = stack?.split("\n").map((line)=>line.trim()).filter(Boolean).slice(0, STACK_LINES);
68
+ return {
69
+ message: error.message,
70
+ stack: frames?.length ? frames.join("\n") : undefined
71
+ };
72
+ }
73
+ function isLocalHost(input) {
74
+ return LOCAL_HOSTS.has(input.event.url.hostname);
75
+ }
76
+ async function report(options, input, extraTags) {
77
+ const { dsn, dev, release, surface, environment = "production", tags } = options;
78
+ if (dev || !dsn || isLocalHost(input)) return;
79
+ await captureError({
80
+ dsn,
81
+ error: toError(input.error),
82
+ surface,
83
+ release,
84
+ environment,
85
+ tags: {
86
+ ...tags,
87
+ ...extraTags,
88
+ route: input.event.route.id
89
+ },
90
+ extra: {
91
+ pathname: input.event.url.pathname,
92
+ kind: input.kind
93
+ }
94
+ });
95
+ }
96
+ async function respondTo(options, input) {
97
+ if (!options.respond) return undefined;
98
+ try {
99
+ return await options.respond(input);
100
+ } catch (err) {
101
+ console.error("[handleError] respond threw:", err);
102
+ return undefined;
103
+ }
104
+ }
105
+ async function answer(options, input) {
106
+ const body = await respondTo(options, input);
107
+ if (!options.dev || !shouldReport(input)) return body;
108
+ return {
109
+ ...body,
110
+ ...devErrorDetails(input.error)
111
+ };
112
+ }
113
+ export function createHandleClientError(options) {
114
+ return async (input)=>{
115
+ try {
116
+ let tags;
117
+ if (isStaleChunk(input.error)) {
118
+ if (reloadForStaleChunk()) return respondTo(options, input);
119
+ tags = {
120
+ stale_chunk: true
121
+ };
122
+ }
123
+ if (shouldReport(input)) {
124
+ if (options.log !== false) console.error("[handleError]", input.error);
125
+ if (!await options.skip?.(input)) await report(options, input, tags);
126
+ }
127
+ } catch (err) {
128
+ console.error("[handleError] reporting failed:", err);
129
+ }
130
+ return answer(options, input);
131
+ };
132
+ }
133
+ export function createHandleServerError(options) {
134
+ return async (input)=>{
135
+ try {
136
+ if (shouldReport(input)) {
137
+ if (options.log !== false) console.error("[handleError]", toError(input.error));
138
+ await report(options, input);
139
+ }
140
+ } catch (err) {
141
+ console.error("[handleError] reporting failed:", err);
142
+ }
143
+ return answer(options, input);
144
+ };
145
+ }
package/dist/format.js ADDED
@@ -0,0 +1,62 @@
1
+ export function formatDate(date, locale = "en-US") {
2
+ if (!date) return "-";
3
+ const dateValue = typeof date === "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date;
4
+ const parsed = new Date(dateValue);
5
+ if (Number.isNaN(parsed.getTime())) return "-";
6
+ return parsed.toLocaleDateString(locale, {
7
+ month: "short",
8
+ day: "numeric",
9
+ year: "numeric"
10
+ });
11
+ }
12
+ export function formatCurrency(value, currency = "USD", locale) {
13
+ if (!value) return "-";
14
+ const resolvedLocale = locale ?? (currency.toUpperCase() === "EUR" ? "fr-FR" : "en-US");
15
+ return new Intl.NumberFormat(resolvedLocale, {
16
+ style: "currency",
17
+ currency: currency.toUpperCase(),
18
+ minimumFractionDigits: 0
19
+ }).format(value);
20
+ }
21
+ export function formatAmount(cents, currency, locale = "en-US") {
22
+ return new Intl.NumberFormat(locale, {
23
+ style: "currency",
24
+ currency: currency.toUpperCase(),
25
+ minimumFractionDigits: 2
26
+ }).format(cents / 100);
27
+ }
28
+ const DEFAULT_LABELS = {
29
+ today: "Today",
30
+ yesterday: "Yesterday",
31
+ daysAgo: "{n} days ago",
32
+ weeksAgo: "{n} weeks ago",
33
+ monthsAgo: "{n} months ago",
34
+ yearsAgo: "{n} years ago",
35
+ weekAgo: "1 week ago",
36
+ monthAgo: "1 month ago",
37
+ yearAgo: "1 year ago"
38
+ };
39
+ export function formatRelativeTime(date, labels = DEFAULT_LABELS) {
40
+ if (!date) return "-";
41
+ const dateValue = typeof date === "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date;
42
+ const parsed = new Date(dateValue);
43
+ if (Number.isNaN(parsed.getTime())) return "-";
44
+ const diffMs = Date.now() - parsed.getTime();
45
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
46
+ if (diffDays === 0) return labels.today;
47
+ if (diffDays === 1) return labels.yesterday;
48
+ if (diffDays < 7) return labels.daysAgo.replace("{n}", String(diffDays));
49
+ if (diffDays < 30) {
50
+ const weeks = Math.floor(diffDays / 7);
51
+ if (weeks === 1 && labels.weekAgo) return labels.weekAgo;
52
+ return labels.weeksAgo.replace("{n}", String(weeks));
53
+ }
54
+ if (diffDays < 365) {
55
+ const months = Math.floor(diffDays / 30);
56
+ if (months === 1 && labels.monthAgo) return labels.monthAgo;
57
+ return labels.monthsAgo.replace("{n}", String(months));
58
+ }
59
+ const years = Math.floor(diffDays / 365);
60
+ if (years === 1 && labels.yearAgo) return labels.yearAgo;
61
+ return labels.yearsAgo.replace("{n}", String(years));
62
+ }
package/dist/grab.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Alt+Click any element in a Svelte 5 dev build to copy its HTML and component stack for a coding agent.
3
+ */
4
+
5
+ /*
6
+ * Usage in +layout.svelte: if (dev) import("@heyramzi/utils/grab");
7
+ */
8
+
9
+ /*
10
+ * Plain JS with no imports, so a page can also inline this file as a <script type="module">.
11
+ */
12
+
13
+ const MAX_HTML = 400;
14
+
15
+ /** @param {Element} el */
16
+ function sourceOf(el) {
17
+ /** @type {Element | null} */
18
+ let node = el;
19
+ while (node && !(/** @type {any} */ (node).__svelte_meta)) node = node.parentElement;
20
+ return node ? /** @type {any} */ (node).__svelte_meta : null;
21
+ }
22
+
23
+ /** @param {{ file: string, line: number, column: number }} at */
24
+ function where(at) {
25
+ return `${at.file}:${at.line}:${at.column}`;
26
+ }
27
+
28
+ /** @param {Element} el */
29
+ function describe(el) {
30
+ // Svelte's hydration markers are <!----> comments, noise to an agent.
31
+ const full = el.outerHTML.replace(/<!--[^]*?-->/g, "");
32
+ const html = full.length > MAX_HTML ? `${full.slice(0, MAX_HTML)}…` : full;
33
+ const meta = sourceOf(el);
34
+ if (!meta) return html;
35
+
36
+ const lines = [html, ` at ${where(meta.loc)}`];
37
+ for (let entry = meta.parent; entry; entry = entry.parent) {
38
+ if (entry.type === "component") lines.push(` in <${entry.componentTag}> at ${where(entry)}`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+
43
+ /** @param {Element} el */
44
+ function label(el) {
45
+ const meta = sourceOf(el);
46
+ let entry = meta?.parent;
47
+ while (entry && entry.type !== "component") entry = entry.parent;
48
+ const name = entry ? `<${entry.componentTag}>` : el.tagName.toLowerCase();
49
+ return meta ? `${name} ${meta.loc.file.split("/").pop()}:${meta.loc.line}` : name;
50
+ }
51
+
52
+ function start() {
53
+ const box = document.createElement("div");
54
+ const tag = document.createElement("div");
55
+ box.style.cssText =
56
+ "position:fixed;pointer-events:none;z-index:2147483647;border:2px solid #7c3aed;background:rgb(124 58 237 / 0.08);border-radius:3px;display:none";
57
+ tag.style.cssText =
58
+ "position:absolute;left:-2px;bottom:100%;margin-bottom:4px;padding:2px 6px;font:12px/1.4 ui-monospace,monospace;color:#fff;background:#7c3aed;border-radius:3px;white-space:nowrap";
59
+ box.append(tag);
60
+ document.body.append(box);
61
+
62
+ /** @type {Element | null} */
63
+ let target = null;
64
+
65
+ /** @param {Element | null} el */
66
+ function show(el) {
67
+ target = el;
68
+ if (!el) {
69
+ box.style.display = "none";
70
+ return;
71
+ }
72
+ const r = el.getBoundingClientRect();
73
+ Object.assign(box.style, {
74
+ display: "block",
75
+ left: `${r.left}px`,
76
+ top: `${r.top}px`,
77
+ width: `${r.width}px`,
78
+ height: `${r.height}px`,
79
+ });
80
+ tag.textContent = label(el);
81
+ }
82
+
83
+ /** @param {MouseEvent} e */
84
+ function onMove(e) {
85
+ show(e.altKey ? document.elementFromPoint(e.clientX, e.clientY) : null);
86
+ }
87
+
88
+ /** @param {MouseEvent} e */
89
+ function onClick(e) {
90
+ if (!e.altKey || !target) return;
91
+ // Option+click on a link downloads it in Chrome, so the click is swallowed here.
92
+ e.preventDefault();
93
+ e.stopImmediatePropagation();
94
+ if (e.type !== "click") return;
95
+ navigator.clipboard.writeText(describe(target)).then(() => {
96
+ tag.textContent = "Copied";
97
+ });
98
+ }
99
+
100
+ addEventListener("mousemove", onMove, true);
101
+ addEventListener("mousedown", onClick, true);
102
+ addEventListener("click", onClick, true);
103
+ addEventListener("keyup", (e) => e.key === "Alt" && show(null), true);
104
+ addEventListener("blur", () => show(null));
105
+ }
106
+
107
+ const w = /** @type {any} */ (globalThis);
108
+ if (typeof document !== "undefined" && !w.__heyramziGrab) {
109
+ w.__heyramziGrab = true;
110
+ if (document.body) start();
111
+ else addEventListener("DOMContentLoaded", start);
112
+ }
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { cn } from "./cn.js";
2
+ export { formatDate, formatCurrency, formatAmount, formatRelativeTime } from "./format.js";
3
+ export { slugify, generateSlug, buildSearchParams, parseJSON, parseCommaSeparatedList, matchesQuery } from "./string.js";
4
+ export { fetchApi } from "./error.js";
5
+ export { isCacheExpired } from "./cache.js";
6
+ export { Ok, Err, tryCatch, tryCatchAsync } from "./result.js";
7
+ export { setLogger, log } from "./logger.js";
8
+ export { debounce, throttle, sleep } from "./timing.js";
9
+ export { clamp } from "./math.js";
10
+ export { groupBy } from "./collection.js";
File without changes
package/dist/logger.js ADDED
@@ -0,0 +1,22 @@
1
+ let _logger = null;
2
+ export function setLogger(logger) {
3
+ _logger = logger;
4
+ }
5
+ export function log(level, message, data) {
6
+ if (!_logger) return;
7
+ const entry = {
8
+ level,
9
+ message,
10
+ ...data
11
+ };
12
+ try {
13
+ const result = _logger.log(entry);
14
+ if (result instanceof Promise) {
15
+ result.catch((err)=>{
16
+ console.error("[logs] Logger failed:", err);
17
+ });
18
+ }
19
+ } catch (err) {
20
+ console.error("[logs] Logger failed:", err);
21
+ }
22
+ }
package/dist/math.js ADDED
@@ -0,0 +1,3 @@
1
+ export function clamp(value, min, max) {
2
+ return Math.min(Math.max(value, min), max);
3
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { tryCatch, tryCatchAsync } from "../result.js";
3
+ const cache = new Map();
4
+ function readRaw(path) {
5
+ const result = tryCatch(()=>readFileSync(path));
6
+ return result.ok ? result.value : null;
7
+ }
8
+ function toArrayBuffer(bytes) {
9
+ const out = new ArrayBuffer(bytes.byteLength);
10
+ new Uint8Array(out).set(bytes);
11
+ return out;
12
+ }
13
+ async function decodeFont(path) {
14
+ const cached = cache.get(path);
15
+ if (cached) return cached;
16
+ const raw = readRaw(path);
17
+ if (!raw) return null;
18
+ const isWoff2 = path.toLowerCase().endsWith(".woff2");
19
+ if (!isWoff2) {
20
+ const buf = toArrayBuffer(raw);
21
+ cache.set(path, buf);
22
+ return buf;
23
+ }
24
+ const decoded = await tryCatchAsync(async ()=>{
25
+ const { decompress } = await import("wawoff2");
26
+ return decompress(raw);
27
+ });
28
+ if (!decoded.ok) return null;
29
+ const buf = toArrayBuffer(decoded.value);
30
+ cache.set(path, buf);
31
+ return buf;
32
+ }
33
+ export async function loadFonts(configs) {
34
+ const decoded = [];
35
+ for (const config of configs){
36
+ const data = await decodeFont(config.path);
37
+ if (!data) continue;
38
+ decoded.push({
39
+ name: config.name,
40
+ data,
41
+ weight: config.weight,
42
+ style: config.style ?? "normal"
43
+ });
44
+ }
45
+ return decoded;
46
+ }
@@ -0,0 +1,3 @@
1
+ export { renderImage, MIME_TYPES } from "./renderer.js";
2
+ export { loadFonts } from "./fonts.js";
3
+ export { html, raw } from "./markup.js";